caveman2-widgets

2017-05-16

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' approach: 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 through a given context.

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!

Quick overview where caveman2-widgets is in the caveman2 ecosystem

Installation

You can use caveman2-widgets with Quicklisp!

(ql:quickload :caveman2-widgets)

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.

See also

caveman2-widgets-bootstrap
Introduces new widgets that use Bootstrap.
caveman2-widgets-blog
An example application to demonstrate caveman2-widgets

Websites running caveman2-widgets

Free-your-PC
My personal website where I have among others a web shop

Let me know if you use it too, to include you here!

Contributions

You 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 dark create either an Issues or mail me. Maybe your feature is on my agenda too.
  • Showing your appreciation through a donation (please mail me for my IBAN). It may be a donation in kind too! Via PayPal you can donate to: richard.baeck@free-your-pc.com

If you add new features, please document them. Otherwise other developers will have a hard time using this framework.

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

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.

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. Use MAKE-WIDGET with :GLOBAL to get a globally scoped widget.

(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!"))

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 within the session (but not as user of this framework). :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 "/" ()
  (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-widget :global '<button-widget>
                           :label "Reset session"
                           :callback #'(lambda ()
                                         (remove-widget-for-session 
                                          :session-widget))))))

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

Some default widgets and layouts

There are some helpful default widgets which may help you with your code organisation. These are:
<COMPOSITE-WIDGET>
A layout which contains widgets that will be rendered vertically.
<HCOMPOSITE-WIDGET>
A layout like the <COMPOSITE-WIDGET> but renders the widgets horizontally.
<BORDER-WIDGET>
A layout which features sections to put widgets in. Please note that this widget has styles in /static/css/widgets.css.
<STRING-WIDGET>
A widget which renders only a string.
<FUNCTION-WIDGET>
A widget which uses a supplied function for rendering. Therefore the supplied function has to return a string!

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>")))))

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.

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). In any case the callback function gets called - please keep that in mind.

will be reloaded entirely or, if JavaScript is enabled, the dirty widgets will be reloaded. Please leave out the starting "/" If you want to address a target on the localhost. E.g. you are on the page "/test", then return "test" if you want to stay on it.

default a symbol generated by GENSYM. But you can change that by giving your <CALLBACK-WIDGET> a specific ID (like in the example below). This will ensure that the route will persist otherwise the route for the <CALLBACK-WIDGET> will change with every restart of your website or with every new session (depends on the scope). *Be careful, the ID must be unique on object level, otherwise you overwrite routes!*

(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.

Use caveman2-widgets for your entire HTML document

To make your life really easy you can create an entire HTML document. You can either tinker your own widgets or whatever with the <HMTL-DOCUMENT-WIDGET> and the <HEADER-WIDGET> or you can use the handy 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*)))

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 and sessioned widgets.

(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*)))

Navigation objects

You can create navigation objects too! The purpose of navigation objects is that you don't have to manage a navigation ever 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). Please keep in mind that navigation objects are session stateful widgets.

item in the list is the widget which will be displayed at the base path of the navigation. You can use any string as path but be careful to not interfere with the special paths of NINGLE (e.g. "/:some-path"). Do not use those. The only special path you can use is the wildcard (e.g "*").

(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.

<MENU-NAVIGATION-WIDGET>
A navigation with a menu. You can change the menu appearance with CSS. With the :HIDDEN keyword you can hide a path from the navigation list.
<BLANK-NAVIGATION-WIDGET>
A navigation without any menu. It is controlled by the URL only - or by other widgets.

Table objects

You can create a table very simple. A <TABLE-WIDGET> displays all items which are supplied through the PRODUCER function.

function. The function should return a list of <TABLE-ITEM> objects. This function can be anything but it has to take the key arguments:
AMOUNT
Tells how many items to get
ALREADY
Tells how many items already received
LENGTH-P
A flag which should tell the function to return the available items if active.

AMOUNT and ALREADY can be seen as synonyms for FROM and TO.

objects is that they can be translated to lists through the generic function GET-AS-LIST. Therefore you don't have to subclass <TABLE-ITEM> at all just to add an implementation of GET-AS-LIST for your used class.

(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*)))

Viewgrids

The <VIEWGRID-WIDGET> is used to display a bulk of heterogenous items. The items must implement the RENDER-AS method. The <VIEWGRID-WIDGET> calls RENDER-AS with its VIEW slot. Therefore you have provide an implementation for the keyword supplied by VIEW in your <VIEWGRID-WIDGET>.

slot. If this slot is active the items are delivered on several pages instead on only one. If you supply additionally the DISPLAY-SELECTOR with the URI path on which the <VIEWGRID-WIDGET> object is rendered, then selectable page numbers are displayed on the bottom too.

given function is called with the item as parameter.

(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))))

Forms

Forms can be pretty annoying but with the <FORM-WIDGET> you don't have to 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>.

<INPUT-FIELD>
Is basically an abstraction of the HTML input-tag.
<SELECT-FIELD>
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.

available slots for <FORM-FIELD> objects is necessary:
REQUIRED
A non-nil value indicates that this field has to have some value.
SUPPLIED
Will be set NIL by SET-REQUIRED-PRESENT and set T by RENDER-WIDGET. It is NIL if the field is not supplied and is therefore not dependent on REQUIRED. It should tell the server whether an parameter was passed or not.
CHECK-FUNCTION
Will be called by SET-REQUIRED-PRESENT and check if the passed value by the client is "correct". It is a lambda with one argument, which is the passed string from the client. Should return NIL if the passed string was not correct and a non-nil value otherwise.
ERROR-HAPPENED
Will be set to T by SET-REQUIRED-PRESENT if the CHECK-FUNCTION did not succeed. The rendering the form will set it to NIL again.
ERROR-MESSAGE
The message that will be displayed if ERROR-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*.

(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*)))

Protecting widgets

This library also enables you to protect widgets. Each widget has an associated list of keywords which indicate the levels/circles of authorization an requester needs.

means that everybody can access your widget. If the protection is non-nil the non-nil value is a list of keywords which refers to a list of keywords stored in the session. So if the session contains the required keyword in its list the requester can access the widget. Otherwise he is denied (throws a 403 code).

indicates the needed token in the session. But caveman2-widgets supplies an additional, specific PROTECT-WIDGET method which should be used. You can supply the following parameters:
:LOGIN
Protects the widget by the default login-widget
A keyword in general
Protects the widget with this keyword (adds it)
A list of keywords
Protects the widget with this keywords (adds them)
(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.")

Login

Protecting certain widgets by a login is very easy. The <LOGIN-WIDGET> organizes the following things:
  1. It displays a login form and logs the client in if he passes the challenge. The successful pass sets will result in an permanent non-nil value if you call "(LOGGED-IN SESSION)". This means that every widget that requires the authroization level :LOGIN through the PROTECT-WIDGET method can now be accessed by the user.
  2. It supplies a Logout button. This button can be access through the LOGOUT-BUTTON reader. You therefore can render the button anywhere you like. Pressing the button will result in a logout and therefore in a permanent NIL for "(LOGGED-IN SESSION)".
  3. It renders certain widgets if the login was successful. This can be either used e.g. for a success message.

*The <LOGIN-WIDGET> has to run in :SESSION scope!*

(authentication functions) if you wish. But using the <LOGIN-WIDGET> and passing the challenge will only set the authoriatzition level to :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)))))))

Language getting/setting

To store the accpeted languages in the session there is the CHECK-AND-SET-LANGUAGE function. This function uses the value supplied through the "Accept-languages" field in the HTTP request. It gets called through the render method by any <HTML-DOCUMENT-WIDGET> automatically. Which means that as soon as you use it you don't have to worry about getting the language. But on the other hand you have to make sure that every subclass of <HTML-DOCUMENT-WIDGET> again uses CHECK-AND-SET-LANGUAGE in its render-method.

ACCEPTED-LANGUAGES.

AUTOMATICALLY-SET-LANGUAGES as NIL to the INIT-WIDGETS function. Please then use the setf method for ACCEPTED-LANGUAGES to set the language.

Translations

Most strings that are rendered human readable get translated through a special function. You can specify your own translation function by passing it to INIT-WIDGETS as :TRANSLATION-FUNCTION. The function header 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>

Development hooks

In case you want to do things at compile time (e.g. calling DEFROUTE) whe INIT-WIDGETS is evaluated there is the variable INIT-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")))))

Important notes/Things that happen automatically

The following things you should keep in mind when using caveman2-widgets.

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))

Buttons and Links are not accessed through the REST path (see the section above).

Widgets that are not accessible through the REST:
  • <HTML-DOCUMENT-WIDGET>
  • <HEADER-WIDGET>

Encapsulating widgets with divs

Each widget gets wrapped in a div automatically. Every widget will get its entire class heritage included in the CSS class attribute. 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 manually add jQuery and all the JavaScript Code supplied/needed by 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)*

CSS dependencies

As with JavaScript, stylesheets are added in the <HEADER-WIDGET> automatically too. The routes you need in your head tag are:
  • /widgets/css/widgets.css

Session values

This section should inform you about keywords in the session variable which you should absolutely not modify.
:WIDGET-HOLDER
<WIDGET-HOLDER> object. It holds all the session scoped widgets.
:DIRTY-OBJECT-IDS
The name says it all.
:JAVASCRIPT-AVAILABLE
Holds a boolean value if JavaScript is available or not.
:ACCEPT-LANGUAGE
Holds the languages accepted by the client.

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.

Author
Richard Paul B?ck
License
LLGPL