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!
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
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")
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")
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*))
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.
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*)))
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*)))
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*))))
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).
Each widget gets wrapped in a div automatically. Therefore you can access every widget (and derived widget) very easily with CSS.
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)*
This section should inform you have session keywords which you should absolutely not modify.
- :WIDGET-HOLDER
- :DIRTY-OBJECT-IDS
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.
- Richard Paul Bäck (richard.baeck@free-your-pc.com)
Copyright (c) 2016 Richard Paul Bäck (richard.baeck@free-your-pc.com)
Licensed under the LLGPL License.