Weblocks like widgets for caveman2.
1.2InstallationYou can use caveman2-widgets with Quicklisp!
If you want to contribute or be always up to date you can clone this git-repository into "~/quicklisp/local-projects" or (if you are using Roswell) "~/.roswell/local-projects" to QUICKLOAD it.
- Introduces new widgets that useBootstrap.
- An example application to demonstratecaveman2-widgets
1.4Websites running caveman2-widgets
- My personal website where I have among others a webshop
Let me know if you use it too, to include you here!
1.5ContributionsYou are very welcomed to contribute to this project! You can contribute by:
- Using it and spreading the word!
- Finding flaws and submitting Issues.
- Finding flaws and removing them (as Pull-requests).
- Adding new features (as Pull-requests). Before shooting in the darkcreate either an Issues or mail me. Maybe your feature is on myagenda too.
- Showing your appreciation through a donation (please mail me for myIBAN). It may be a donation in kind too! Via PayPal you can donateto: email@example.com
If you add new features, please document them. Otherwise other developers will have a hard time using this framework.
1.6.1GeneralThe only important thing is to run the function INIT-WIDGETS with an<APP>. If you use caveman's MAKE-PROJECT function you will get filecalled "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
If you create objects from your widget classes, then please always use the MAKE-WIDGET function! This method should be used, since it does all the background stuff for you.
1.6.2Global scopeThere are two scopes: global and session. The global scope"limits" the widget to all users. Therefore if you create a statefulwidget the state will be displayed to all users of your site. UseMAKE-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")
A good practice to create disposable widgets is to mark them :GLOBAL. In the following example the widget will be created when a user connects and will afterwards immediately be destroyed again by the garbage collector.
(defroute "/" () (render-widget (make-widget :global '<string-widget> :text "Hello world!"))
1.6.3Session scopeThe other option is to use a session scope. This is a bit moretricky because all your session widgets must be stored within thesession (but not as user of this framework). :SESSION is the keywordfor MAKE-WIDGET to get a session widget. Of course you only need tosave the top level (highest) widget of a widget tree in the session(the children will be saved where the parent is). A short overview ofthe functions:
- Saves a widget in the sessionvariable. This should be considered ONLY for session scopedwidgets.
- Gets a previously saved widget from thesession variable (e.g. to render it).
- Removes a saved widget from the sessionvariable.
An example (with children):
1.6.4Some default widgets and layoutsThere are some helpful default widgets which may help you with yourcode organisation. These are:
- A layout which contains widgets that will be renderedvertically.
- A layout like the <COMPOSITE-WIDGET> but renders thewidgets horizontally.
- A layout which features sections to put widgetsin. Please note that this widget has styles in/static/css/widgets.css.
- A widget which renders only a string.
- A widget which uses a supplied function forrendering. Therefore the supplied function has to return astring!
A simple example:
(defroute "/composite" () (with-html-document (doc (make-instance '<header-widget>)) (setf (body doc) (make-widget :global '<border-widget> :east (make-widget :global '<string-widget> :text "<h2>Hello from east</h2>") :center (make-widget :global '<hcomposite-widget> :widgets (list (make-widget :global '<string-widget> :text "<h1>Hello from left</h1>") (make-widget :global '<function-widget> :function #'(lambda () "<h1>Hello from the mid</h1>")) (make-widget :global '<string-widget> :text "<h1>Hello from right</h1>"))) :west (make-widget :global '<string-widget> :text "<h2>Hello from west</h2>")))))
(defvar *got-here-by-link* nil) (defroute "/otherpage" () (if *got-here-by-link* (progn (setf *got-here-by-link* nil) "<h1>Got here by pressing the link</h2>") "<h1>Got here by yourself</h2>")) (defroute "/link-test" () (concatenate 'string (render-widget (make-widget :global '<link-widget> :label "Github" :callback #'(lambda (args) (format t "LOG: Link clicked!") "http://github.com/ritschmaster") :target-foreign-p t ;; The link goes out of this domain )) (render-widget (make-widget :global '<link-widget> :label "Otherpage" :id "otherpage" ;; href="/links/otherpage" :callback #'(lambda (args) (setf *got-here-by-link* t) "/otherpage") :target-foreign-p t ;; The link goes out of this domain )) (render-widget (make-widget :global '<button-widget> :label "Button" :callback #'(lambda (args) (format t "LOG: Button clicked!"))))))
You can create your own callback widgets too. Just look at the <CALLBACK-WIDGET>, <BUTTON-WIDGET> classes for that.
1.6.6Use caveman2-widgets for your entire HTML documentTo make your life really easy you can create an entire HTMLdocument. You can either tinker your own widgets or whatever with the<HMTL-DOCUMENT-WIDGET> and the <HEADER-WIDGET> or you can use thehandy WITH-HTML-DOCUMENT macro.
(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> ;; the title when this header is used :title "Widgets test" ;; the icon when this header is used :icon-path "/images/icon.png" ;; the following lines will be rendered in the header: :other-header-content '("<meta name=\"author\" content=\"Richard Bäck\">")) (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> ;; sets this specific header for this page :header *header-widget* :body *root-widget*))) (defroute "/otherpage" () (with-html-document (doc *header-widget*) (setf (body doc) *otherpage-widget*)))
(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-widget :global '<button-widget> :label "Enable" :callback #'(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-widget :global '<button-widget> :label "Disable" :callback #'(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" () (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*)))
(defvar *first-widget* (make-widget :global '<string-widget> :text "<h1>Hello world from first</h1>")) (defvar *second-widget* (make-widget :global '<string-widget> :text "<h1>Hello world from second</h1>")) (defclass <proxy-widget> (<widget>) () (:documentation "This class enables session based widgets for a navigation.")) (defmethod render-widget ((this <proxy-widget>)) (set-widget-for-session :string-widget (make-widget :session '<string-widget> :text "hello world")) (render-widget (get-widget-for-session :string-widget))) (defnav "/sophisticated/path" ((make-instance '<header-widget> :title "Navigation test") (list (list "First widget" "first" *first-widget*) (list "Second widget" "second" *second-widget*) (list "Third widget" "third" (make-widget :global '<proxy-widget>)) (list "Hidden widget" "hidden" (make-widget :global '<string-widget> :text "<h1>You have accessed a hidden widget!</h1>") :hidden)) :kind '<menu-navigation-widget>))
If the default navigation object doesn't render as you wish, you can subclass it and overwrite the RENDER-WIDGET method. Please notice that you can actually very easily adjust the path where the navigation and its widgets get rendered. The slot BASE-PATH is created for that.There are two default navigation widgets:
- A navigation with a menu. You can changethe menu appearance with CSS. With the :HIDDEN keyword you canhide a path from the navigation list.
- A navigation without any menu. It iscontrolled by the URL only - or by other widgets.
1.6.9Table objectsYou can create a table very simple. A <TABLE-WIDGET> displays allitems which are supplied through the PRODUCER function.Important for the usage of tables is that you supply a PRODUCERfunction. The function should return a list of <TABLE-ITEM>objects. This function can be anything but it has to take the keyarguments:
- Tells how many items to get
- Tells how many items already received
- A flag which should tell the function to return theavailable items if active.
(defclass <my-item> (<table-item>) ((id :initarg :id :reader id) (name :initarg :name :reader name) (description :initarg :description :reader description))) (defmethod get-as-list ((this <my-item>)) (list :id (id this) :name (name this) :description (description this))) (defun producer (&key amount (already 0) (length-p nil)) (if (null length-p) (let ((all '())) (if (null amount) (loop for x from 1 to 1000 do (setf all (append all (list (make-instance '<my-item> :id x :name (format nil "~a" x) :description (format nil "The ~a. item." x)))))) (loop for x from (+ already 1) to (+ already amount) do (setf all (append all (list (make-instance '<my-item> :id x :name (format nil "~a" x) :description (format nil "The ~a. item." x))))))) all) 1000)) (defvar *table-widget* (make-widget :global '<table-widget> :producer 'producer :column-descriptions (list (list :name "Name") (list :description "Description")))) (defroute "/table" () (with-html-document (doc (make-instance '<header-widget>)) (setf (body doc) *table-widget*)))
1.6.10ViewgridsThe <VIEWGRID-WIDGET> is used to display a bulk of heterogenousitems. The items must implement the RENDER-AS method. The<VIEWGRID-WIDGET> calls RENDER-AS with its VIEW slot. Therefore youhave provide an implementation for the keyword supplied by VIEW inyour <VIEWGRID-WIDGET>.You can limit the displayed items with the MAX-ITEMS-TO-DISPLAYslot. If this slot is active the items are delivered on several pagesinstead on only one. If you supply additionally the DISPLAY-SELECTORwith the URI path on which the <VIEWGRID-WIDGET> object is rendered,then selectable page numbers are displayed on the bottom too.Each item can be accessed. When accessing the item a specificgiven function is called with the item as parameter.The following example covers all functionality:
(defclass <my-viewgrid-item> (<viewgrid-item>) ((id :initarg :id :reader id) (name :initarg :name :reader name) (description :initarg :description :reader description))) (defmethod render-as ((this <my-viewgrid-item>) (view (eql :short))) (format nil "<div style=\"padding-bottom:30px\">id: ~a<br>name: ~a<br>desc: ~a<div>" (id this) (name this) (description this))) (defun producer (&key (from 0) (to nil) (length-p nil)) (let ((all '())) (loop for x from 1 to 35 do (setf all (append all (list (make-instance '<my-viewgrid-item> :id x :name (format nil "~a" x) :description (format nil "The ~a. item." x)))))) (cond (length-p (length all)) ((and from (not to)) (mapcan #'(lambda (item) (if (>= (id item) from) (list item) nil)) all)) ((and from to) (mapcan #'(lambda (item) (if (and (>= (id item) from) (< (id item) to)) (list item) nil)) all))))) (defroute "/viewgrid" () (with-html-document (doc (make-instance '<header-widget>)) (set-widget-for-session :viewgrid (make-widget :session '<viewgrid-widget> :producer #'producer :view :short :max-items-to-display 11 :display-selector "viewgrid" :on-view #'(lambda (item) (format t (render-as item :short)) "viewgrid"))) (setf (body doc) (get-widget-for-session :viewgrid))))
1.6.11FormsForms can be pretty annoying but with the <FORM-WIDGET> you don't haveto care for anything but for the naming of the inputs ever again. Each<FORM-WIDGET> consists of 0 to n <FORM-FIELD> objects. If you have 0<FORM-FIELD> objects it essentially only behaves like a<BUTTON-WIDGET>.<FORM-FIELD> is the base class for fields. Fields can be:
- Is basically an abstraction of the HTML input-tag.
- Consists of <OPTION-FIELD> objects.
Of course you can implement your own <FORM-FIELD> classes too! But keep in mind that the default <FORM-FIELD> already implements constraints.To understand how constraints for forms work an examination of theavailable slots for <FORM-FIELD> objects is necessary:
- A non-nil value indicates that this field has to havesome value.
- Will be set NIL by SET-REQUIRED-PRESENT and set T byRENDER-WIDGET. It is NIL if the field is not suppliedand is therefore not dependent on REQUIRED. It shouldtell the server whether an parameter was passed or not.
- Will be called by SET-REQUIRED-PRESENT and checkif the passed value by the client is "correct". Itis a lambda with one argument, which is the passedstring from the client. Should return NIL if thepassed string was not correct and a non-nil valueotherwise.
- Will be set to T by SET-REQUIRED-PRESENT if theCHECK-FUNCTION did not succeed. The rendering theform will set it to NIL again.
- The message that will be displayed ifERROR-HAPPENED is T.
You don't have to actually care for that procedure as the <FORM-WIDGET> calls this the SET-REQUIRED-PRESENT by itself. But it can be helpful to understand the entire process of checking the user input. The only thing to really memorize here is that the given callback only gets called if all required fields where supplied and those fields where supplied correctly.Consider the following example for additional help:
(defvar *password-field* (make-instance '<input-field> :input-type "password" :check-function #'(lambda (pass) (if (<= (length pass) 2) nil t)) :error-message "Has to be longer than 2" :name "password" :value "")) (defvar *form-widget* (let ((text-field (make-instance '<input-field> :input-type "text" :name "text" :value "" :required t)) (choice-field (make-instance '<select-field> :name "selection" :options (list (make-instance '<option-field> :value "first") (make-instance '<option-field> :value "second" :display-value "Other"))))) (make-widget :global '<form-widget> :input-fields (list text-field *password-field* choice-field) :label "Submit" :callback #'(lambda (args) (format t "received correct values: ~a -------------" args))))) (defroute "/form" () (with-html-document (doc (make-instance '<header-widget>)) (setf (body doc) *form-widget*)))
1.6.12Protecting widgetsThis library also enables you to protect widgets. Each widget has anassociated list of keywords which indicate the levels/circles ofauthorization an requester needs.By default the protection is an empty list (therefore NIL), whichmeans that everybody can access your widget. If the protection isnon-nil the non-nil value is a list of keywords which refers to a listof keywords stored in the session. So if the session contains therequired keyword in its list the requester can access thewidget. Otherwise he is denied (throws a 403 code).The <WIDGET> class holds the PROTECTED slot. This slots valueindicates the needed token in the session. But caveman2-widgetssupplies an additional, specific PROTECT-WIDGET method which should beused. You can supply the following parameters:
- Protects the widget by the default login-widget
- A keyword in general
- Protects the widget with this keyword (addsit)
- A list of keywords
- Protects the widget with this keywords (addsthem)
(defvar *specific-protected-widget* (protect-widget (make-widget :global '<string-widget> :text "<h1>This is a protected text</h1>") :myprotection)) ;; Should throw 403 (defroute "/protected-widget" () (concatenate 'string "<a href=\"/rest/string-widget?id=" (id *specific-protected-widget*) "\">Will throw 403</a>" (render-widget *specific-protected-widget*))) (defmethod on-exception ((app <web>) (code (eql 403))) (declare (ignore app)) "403 - The protection works.")
1.6.13LoginProtecting certain widgets by a login is very easy. The <LOGIN-WIDGET>organizes the following things:
- It displays a login form and logs the client in if he passes thechallenge. The successful pass sets will result in an permanentnon-nil value if you call "(LOGGED-IN SESSION)". This means thatevery widget that requires the authroization level :LOGIN throughthe PROTECT-WIDGET method can now be accessed by the user.
- It supplies a Logout button. This button can be access through theLOGOUT-BUTTON reader. You therefore can render the button anywhereyou like. Pressing the button will result in a logout and thereforein a permanent NIL for "(LOGGED-IN SESSION)".
- It renders certain widgets if the login was successful. This can beeither used e.g. for a success message.
The <LOGIN-WIDGET> has to run in :SESSION scope!Additionally you can specify different authentication challanges(authentication functions) if you wish. But using the <LOGIN-WIDGET>and passing the challenge will only set the authoriatzition levelto :LOGIN. This means that you need to create your own <LOGIN-WIDGET>if you want some other level for different authentication functions!
(defvar *protected-widget* (protect-widget (make-widget :global '<string-widget> :text "<h1>This is a protected text</h1>") :login)) (defroute "/" () (with-html-document (doc (make-instance '<header-widget>)) (setf (body doc) (make-widget :global '<function-widget> :function #'(lambda () (set-widget-for-session ;; running it in the session :login-widget (make-widget :session '<login-widget> :authenticator #'(lambda (user password) (if (and (string= user "ritschmaster") (string= password "secret")) t nil)) :widgets (list *protected-widget*))) (render-widget (get-widget-for-session :login-widget)))))))
1.6.14Language getting/settingTo store the accpeted languages in the session there is theCHECK-AND-SET-LANGUAGE function. This function uses the value suppliedthrough the "Accept-languages" field in the HTTP request. It getscalled through the render method by any <HTML-DOCUMENT-WIDGET>automatically. Which means that as soon as you use it you don't haveto worry about getting the language. But on the other hand you have tomake sure that every subclass of <HTML-DOCUMENT-WIDGET> again usesCHECK-AND-SET-LANGUAGE in its render-method.You can access the currently accepted languages through theACCEPTED-LANGUAGES.If you rather use a manual language chooser you can supplyAUTOMATICALLY-SET-LANGUAGES as NIL to the INIT-WIDGETSfunction. Please then use the setf method for ACCEPTED-LANGUAGES toset the language.
1.6.15TranslationsMost strings that are rendered human readable get translated through aspecial function. You can specify your own translation function bypassing it to INIT-WIDGETS as :TRANSLATION-FUNCTION. The functionheader should look like this:
(defvar *my-translation-function* #'(lambda (text &key plural-p genitive-form-p items-count accusative-form-p language &allow-other-keys) text))
Strings that are translated:
- The page names of a navigation
Strings that are definitely not translated:
- The TEXT of a <STRING-WIDGET>
- The return value of a <FUNCTION-WIDGET>
1.6.16Development hooksIn case you want to do things at compile time (e.g. calling DEFROUTE)whe INIT-WIDGETS is evaluated there is the variableINIT-WIDGETS-HOOKS. Just append new functions as you wish.
(setf *init-widgets-hooks* (append *init-widgets-hooks* (list #'(lambda () (defroute "/something" () ;; Accessing the user supplied <APP> object: (describe caveman2-widgets::*web*) "something")))))
1.7Important notes/Things that happen automaticallyThe following things you should keep in mind when usingcaveman2-widgets.
1.7.1Automatically REST API creationIf you create a widget then routes for a REST API will be addedautomatically. Suppose you subclass <WIDGET> with the class<MY-WIDGET>, then you will get the path "/rest/my-widget" which youcan 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))
Buttons and Links are not accessed through the REST path (see the section above).
Widgets that are not accessible through the REST:
1.7.2Encapsulating widgets with divsEach widget gets wrapped in a div automatically. Every widget will getits entire class heritage included in the CSS classattribute. Therefore you can access every widget (and derived widget)very easily with CSS.
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).
1.7.5Session valuesThis section should inform you about keywords in the session variablewhich you should absolutely not modify.
- <WIDGET-HOLDER> object. It holds all the sessionscoped widgets.
- The name says it all.
- Holds the languages accepted by the client.
- Richard Paul Bäck (firstname.lastname@example.org)
Copyright (c) 2016 Richard Paul Bäck (email@example.com)
Licensed under the LLGPL License.