ejbs / caveman2-widgets

Enables Weblocks like widgets for caveman2

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

caveman2-widgets

What is it

caveman2-widgets is an extension library to caveman2. It is influenced by Weblocks and introduces its widget system for the developer. By only using its widget concept it does not control the developer as much as Weblocks itself. For people who don’t now Weblocks’ aproach: the developer can create web applications (more) like a normal GUI application by using subclassable widgets which can have callbacks and their like. Each Widget is only responsible for its own content but might interfere with other objects. But the really cool part is that the framework automatically creates your site for dynamically (JavaScript based) access and normal access. For the dynamic approach this means that you don’t have to manage or even care to refresh parts of your website, because widgets can do that by themselves!

Usage

General

The only important thing is to run the function INIT-WIDGETS with an <APP>. If you use caveman’s MAKE-PROJECT function you will get file called “src/web.lisp”. In this file you can adapt the following:

(defpackage my-caveman2-webapp.web
  (:use :cl
        :caveman2
        :caveman2-widgets ;; easy use of the external symbols of this project
        :my-caveman2-webapp.config
        :my-caveman2-webapp.view
        :my-caveman2-webapp.db
        :datafly
        :sxql)
  (:export :*web*))

;; some other code

;; the following will be generated through MAKE-PROJECT but is very important:
(defclass <web> (<app>) ())
(defvar *web* (make-instance '<web>))
(clear-routing-rules *web*)


;; the neccessary call to initialize the widgets:
(init-widgets *web*)

;; from now on you can do whatever you want

Global scope

There are two scopes: global and session. The global scope “limits” the widget to all users. Therefore if you create a stateful widget the state will be displayed to all users of your site. We supplied the method MAKE-WIDGET to generate new widgets. This method should be used, since it does all the background stuff for you. Use MAKE-WIDGET with :GLOBAL to get a globally scoped widget.

A very simple example of what you can do with it:

(defclass <global-widget> (<widget>)
  ((enabled
    :initform nil
    :accessor enabled)))

(defmethod render-widget ((this <global-widget>))
  (if (enabled this)
      "<h1>enabled!</h1>"
      "<h1>not enabled</h1>"))

(defvar *global-widget* (make-widget :global '<global-widget>))

(defroute "/" ()
  (render-widget *global-widget*))

(defroute "/disable" ()
  (setf (enabled *global-widget*) nil)
  "disabled it")

(defroute "/enable" ()
  (setf (enabled *global-widget*) t)
  "enabled it")

Session scope

The other option is to use a session scope. This is a bit more tricky because all your session widgets must be stored with the session. :SESSION is the keyword for MAKE-WIDGET to get a session widget. Of course you only need to save the top level (highest) widget of a widget tree in the session (the children will be saved where the parent is). A short overview of the functions:

SET-WIDGET-FOR-SESSION
Saves a widget in the session variable. This should be considered ONLY for session scoped widgets.
GET-WIDGET-FOR-SESSION
Gets a previously saved widget from the session variable (e.g. to render it).
REMOVE-WIDGET-FOR-SESSION
Removes a saved widget from the session variable.

An example (with children):

(defclass <display-id-widget> (<widget>)
  ())

(defmethod render-widget ((this <display-id-widget>))
  (concatenate 'string
               "<h3>display-id-widget id: <a href=\"/rest/display-id-widget?id="
               (caveman2-widgets.widget::id this)
               "\">"
               (caveman2-widgets.widget::id this)
               "</a></h3>"))

(defclass <session-widget> (<widget>)
  ((id-widget
    :initform (make-widget :session '<display-id-widget>)
    :reader id-widget)))

(defmethod render-widget ((this <session-widget>))
  (concatenate 'string
               "<h1>The id of your widget</h1>"
               "<h2>It should be different for each session</h2>"
               "<p>My id: <a href=\"/rest/session-widget?id="
               (caveman2-widgets.widget::id this)
               "\">"
               (caveman2-widgets.widget::id this)
               "</a></p>"
               (render-widget (id-widget this)))) 

(defroute "/" ()
  (make-widget :session '<widget>) ;; init session
  (set-widget-for-session :session-widget
                          (make-widget :session '<session-widget>))
  (concatenate 'string
               "<head>
<script src=\"https://code.jquery.com/jquery-2.2.2.min.js\" type=\"text/javascript\"></script>
<script src=\"/widgets/js/widgets.js\" type=\"text/javascript\"></script>
</head>"

             (render-widget
              (get-widget-for-session :session-widget))
             (render-widget
              (make-button :global
                           "Reset session"
                           #'(lambda ()
                               (remove-widget-for-session :session-widget))))))

(defroute "/reset-session" ()
  (remove-widget-for-session :session-widget)
  "reset your session")

Some default widgets

There are some helpful default widgets which may help you with your code organisation. These are:

<COMPOSITE-WIDGET>
Contains multiple widgets which will be rendered vertically.
<STRING-WIDGET>
A widget which renders only a string.

A simple example:

(defvar *composite-widget*
  (let ((composite (make-widget :global '<composite-widget>))
        (first (make-widget :global '<string-widget>))
        (second (make-widget :global '<string-widget>)))
    (setf (text first) "<h1>Composite text</h1>")
    (setf (text second)
          "<p>This site contains two string widgets that are wrapped
in a composite widget</p>")
    (append-item composite first)
    (append-item composite second)
    composite))

(defroute "/composite-test" ()
  (render-widget *composite-widget*))

Buttons and links

You can use buttons and links that call specific functions. When you create a button/link only for a session the created route will be guarded. Therefore only the user with the associated route may actually access his button.

An example:

(defroute "/" ()
  (concatenate 'string
               (render-widget
                (make-link :global
                           "Github"
                           #'(lambda ()
                               (format t "LOG: Link clicked!")
                               "http://github.com/ritschmaster")))
               (render-widget
                (make-button :global
                             "Button"
                             #'(lambda ()
                                 (format t "LOG: Button clicked!"))))))

You can create your own callback widgets too. Just look at the <CALLBACK-WIDGET>, <BUTTON-WIDGET> classes and the function MAKE-BUTTON for that.

Use caveman2-widgets for your entire HTML document

To make your life really easy you can create an entire HTML document.

(defclass <root-widget> (<body-widget>)
  ())

(defmethod render-widget ((this <root-widget>))
  "Hello world!")

(defclass <otherpage-widget> (<body-widget>)
  ())

(defmethod render-widget ((this <otherpage-widget>))
  "Hello from the other page!")

(defvar *header-widget* (make-instance '<header-widget>
                                       :title "Widgets test"))
(defvar *root-widget* (make-widget :global '<root-widget>))
(defvar *otherpage-widget* (make-widget :global '<otherpage-widget>))

(defroute "/" ()
  ;; The *root-widget* can be accessed under:
  ;; /rest/root-widget?id=(caveman2-widgets.widget::id *root-widget*)
  (render-widget
   (make-instance '<html-document-widget>
                  :header *header-widget*
                  :body *root-widget*)))
(defroute "/otherpage" ()
  (render-widget
   (make-instance '<html-document-widget>
                  :header *header-widget*
                  :body *otherpage-widget*)))

Marking widgets dirty

You can mark specific widgets as dirty with the function MARK-DIRTY. This means that they will be reloaded dynamically (if the user has JavaScript is enabled). Please notice that you can mark any widget as dirty, therefore you can order JavaScript to reload global widgets as sessioned widgets.

An example:

(defclass <sessioned-widget> (<widget>)
  ((enabled
    :initform nil
    :accessor enabled)))

(defmethod render-widget ((this <sessioned-widget>))
  (concatenate 'string
               "<h2>Sessioned-widget:</h2>"
               (if (enabled this)
                   "<h3>enabled!</h3>"
                   "<h3>not enabled</h3>")))


(defclass <my-body-widget> (<widget>)
  ())

(defmethod render-widget ((this <my-body-widget>))
  (concatenate 'string
               "<h1>MARK-DIRTY test</h1>"
               (render-widget
                (get-widget-for-session :sessioned-widget))
               (render-widget
                (make-button
                 :global "Enable"
                 #'(lambda ()
                     (let ((sessioned-widget
                            (get-widget-for-session :sessioned-widget)))
                       (when sessioned-widget
                         (setf (enabled sessioned-widget) t)
                         (mark-dirty sessioned-widget))))))
               (render-widget
                (make-button
                 :global "Disable"
                 #'(lambda ()
                     (let ((sessioned-widget
                            (get-widget-for-session :sessioned-widget)))
                       (when sessioned-widget
                         (setf (enabled sessioned-widget) nil)
                         (mark-dirty sessioned-widget))))))))

(defvar *header-widget* (make-instance '<header-widget>
                                       :title "Mark-dirty test"))
(defvar *my-body-widget* (make-widget :global '<my-body-widget>))

(defroute "/mark-dirty-test" ()
  (make-widget :session '<widget>) ;; init session
  (set-widget-for-session :sessioned-widget (make-widget :session '<sessioned-widget>))
  (render-widget
   (make-instance '<html-document-widget>
                  :header *header-widget*
                  :body *my-body-widget*)))

Navigation objects

You can create navigation objects too! The purpose of navigation objects is that you don’t have to manage a navigation every again! Each navigation object contains another widget which displays the currently selected path. If you click on a navigation link that object is changed and refreshed (either via JavaScript or through the link).

A very basic example:

(defvar *first-widget*
  (let ((ret (make-widget :global '<string-widget>)))
    (setf (text ret) "<h1>Hello world from first</h1>")
    ret))

(defvar *second-widget*
  (let ((ret (make-widget :global '<string-widget>)))
    (setf (text ret) "<h1>Hello world from second</h1>")
    ret))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Usage without the helper macro (not recommended):
;; (defroute "/" ()
;;   (make-widget :session '<widget>) ;; init session
;;   (set-widget-for-session :nav-widget (make-widget :session '<navigation-widget>))
;;   (let ((nav-widget (get-widget-for-session :nav-widget)))
;;     (append-item nav-widget
;;                  (list "First widget" "first" *first-widget*))
;;     (append-item nav-widget
;;                  (list "Second widget" "second" *second-widget*))
;;     (when (null (header nav-widget))
;;       (setf (header nav-widget)
;;             (make-instance '<header-widget>
;;                            :title "Navigation test")))
;;     (render-widget nav-widget)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defroute "/" ()
  (with-navigation-widget (:nav-widget
                           navigation-widget
                           (make-instance '<header-widget>
                                          :title "Navigation test"))
    (append-item navigation-widget
                 (list "First widget" "first" *first-widget*))
    (append-item navigation-widget
                 (list "Second widget" "second" *second-widget*))))

Things that happen automatically

Automatically REST API creation

If you create a widget then routes for a REST API will be added automatically. Suppose you subclass <widget> with the class “<my-widget>”, then you will get the path “/rest/my-widget” which you can access.

(defclass <my-widget> (<widget>)
  ())

(defmethod render-widget ((this <my-widget>))
  "my-widget representation for the website")

(defmethod render-widget-rest ((this <my-widget>) (method (eql :get)) (args t))
  "my-widget representation for the REST.")

(defmethod render-widget-rest ((this <my-widget>) (method (eql :post)) (args t))
  (render-widget this))

For each button there will be an URI like “/buttons/BUTTONID”. You can access buttons via POST only. Links get a URI like “/links/LINKID” and can be accessed either by GET (get a redirect to the stored link) or by POST (return only the value of the link).

Encapsulating widgets with divs

Each widget gets wrapped in a div automatically. Therefore you can access every widget (and derived widget) very easily with CSS.

JavaScript dependencies

When <HEADER-WIDGET> is used all JavaScript dependencies are added automatically. Please notice that these dependecies are needed to ensure that the widgets work properly. If you don’t want to use <HEADER-WIDGET> you have to add jQuery and all the JavaScript Code supplied by this caveman2-widgets.

The routes for the JavaScript files (which have to be included in each HTML file) are:

  • /widgets/js/widgets.js

The jQuery-Version used is 2.2.2 minified. If you want another jQuery file you can specify it with the variable jquery-cdn-link (should be an URL).

*If you forget to use the JavaScript-files widgets might not work or even break. Most likely all dynamic content just won’t work (automatically fallback to non-JS)*

Session values

This section should inform you have session keywords which you should absolutely not modify.

  • :WIDGET-HOLDER
  • :DIRTY-OBJECT-IDS

Installation

The Quicklisp request is pending!

Until then you can clone this repository into “~/quicklisp/local-projects” or (if you are using Roswell) “~/.roswell/local-projects” to QUICKLOAD it.

Author

  • Richard Paul Bäck (richard.baeck@free-your-pc.com)

Copyright

Copyright (c) 2016 Richard Paul Bäck (richard.baeck@free-your-pc.com)

License

Licensed under the LLGPL License.

About

Enables Weblocks like widgets for caveman2


Languages

Language:Common Lisp 95.1%Language:JavaScript 4.9%