1 Introduction

* caveman2-widgets
** What is it
caveman2-widgets is an extension library to [[https://github.com/fukamachi/caveman][caveman2]]. It is influenced
by [[https://github.com/skypher/weblocks][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.
#+LATEX: \\\\
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!
#+CAPTION: Quick overview where caveman2-widgets is in the caveman2 ecosystem
#+ATTR_LATEX: :width 10cm
#+LABEL: fig:overview
[[overview.jpg]]
** Installation
You can use caveman2-widgets with Quicklisp!
#+BEGIN_SRC lisp
(ql:quickload :caveman2-widgets)
#+END_SRC
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
[[https://github.com/roswell/roswell][Roswell]]) "~/.roswell/local-projects" to QUICKLOAD it.
** See also
- [[https://github.com/ritschmaster/caveman2-widgets-bootstrap][caveman2-widgets-bootstrap]] :: Introduces new widgets that use
[[https://getbootstrap.com/][Bootstrap]].
- [[https://github.com/ritschmaster/caveman2-widgets-blog][caveman2-widgets-blog]] :: An example application to demonstrate
caveman2-widgets
** Websites running caveman2-widgets
- [[https://free-your-pc.com][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 [[https://github.com/ritschmaster/caveman2-widgets/issues][Issues]].
- Finding flaws and removing them (as [[https://github.com/ritschmaster/caveman2-widgets/pulls][Pull-requests]]).
- Adding new features (as [[https://github.com/ritschmaster/caveman2-widgets/pulls][Pull-requests]]). Before shooting in the dark
create either an [[https://github.com/ritschmaster/caveman2-widgets/issues][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
. If you use caveman's MAKE-PROJECT function you will get file
called "src/web.lisp". In this file you can adapt the following:
#+BEGIN_SRC lisp
(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 () ())
(defvar *web* (make-instance '))
(clear-routing-rules *web*)
;; the neccessary call to initialize the widgets:
(init-widgets *web*)
;; from now on you can do whatever you want
#+END_SRC
*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.
#+LATEX: \\\\
A very simple example of what you can do with it:
#+BEGIN_SRC lisp
(defclass ()
((enabled
:initform nil
:accessor enabled)))
(defmethod render-widget ((this ))
(if (enabled this)
"

enabled!

"
"

not enabled

"))
(defvar *global-widget* (make-widget :global '))
(defroute "/" ()
(render-widget *global-widget*))
(defroute "/disable" ()
(setf (enabled *global-widget*) nil)
"disabled it")
(defroute "/enable" ()
(setf (enabled *global-widget*) t)
"enabled it")
#+END_SRC
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.
#+BEGIN_SRC lisp
(defroute "/" ()
(render-widget
(make-widget :global '
:text "Hello world!"))
#+END_SRC
*** 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):
#+BEGIN_SRC lisp
(defclass ()
())
(defmethod render-widget ((this ))
(concatenate 'string
"

Hello from east

Hello from left

")
(make-widget :global '
:function
#'(lambda ()
"

Hello from the mid

"))
(make-widget :global '
:text "

Hello from right

")))
:west (make-widget :global '
:text "

Hello from west

")))))
#+END_SRC
*** 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.
#+LATEX: \\\\
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). In any case the callback
function gets called - please keep that in mind.
#+LATEX: \\\\
If the return value of the link matches the current path then the side
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.
#+LATEX: \\\\
The BUTTONID and LINKID are the ID slots of the widget - which is
default a symbol generated by GENSYM. But you can change that by
giving your a specific ID (like in the example
below). This will ensure that the route will persist otherwise the
route for the 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!*
#+LATEX: \\\\
An example:
#+BEGIN_SRC lisp
(defvar *got-here-by-link* nil)
(defroute "/otherpage" ()
(if *got-here-by-link*
(progn
(setf *got-here-by-link* nil)
"

Sessioned-widget:

"
(if (enabled this)
"

enabled!

"
"

not enabled

MARK-DIRTY test

"
(render-widget
(get-widget-for-session :sessioned-widget))
(render-widget
(make-widget
:global '
: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 '
: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 '
:title "Mark-dirty test"))
(defvar *my-body-widget* (make-widget :global '))
(defroute "/mark-dirty-test" ()
(set-widget-for-session :sessioned-widget (make-widget :session '))
(render-widget
(make-instance '
:header *header-widget*
:body *my-body-widget*)))
#+END_SRC
*** 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*.
#+LATEX: \\\\
Paths are only created automatically by the DEFNAV macro. The first
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 "/*/").
#+LATEX: \\\\
A very basic example:
#+BEGIN_SRC lisp
(defvar *first-widget*
(make-widget :global '
:text "

You have accessed a hidden widget!

")
:hidden))
:kind '))
#+END_SRC
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.
#+LATEX: \\\\
There are two default navigation widgets:
- :: 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.
- :: 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 displays *all*
items which are supplied through the PRODUCER function.
#+LATEX: \\\\
Important for the usage of tables is that you supply a PRODUCER
function. The function should return a list of
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.
#+LATEX: \\\\
A object is needed for tables. The essence of those
objects is that they can be translated to lists through the generic
function GET-AS-LIST. Therefore you don't have to subclass
at all just to add an implementation of GET-AS-LIST for
your used class.
#+LATEX: \\\\
For the consider the following example:
#+BEGIN_SRC lisp
(defclass ()
((id
:initarg :id
:reader id)
(name
:initarg :name
:reader name)
(description
:initarg :description
:reader description)))
(defmethod get-as-list ((this ))
(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 '
: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 '
:id x
:name (format nil "~a" x)
:description (format nil "The ~a. item." x)))))))
all)
1000))
(defvar *table-widget*
(make-widget :global '
:producer 'producer
:column-descriptions (list
(list :name "Name")
(list :description "Description"))))
(defroute "/table" ()
(with-html-document (doc
(make-instance '))
(setf (body doc)
*table-widget*)))
#+END_SRC
*** Viewgrids
The is used to display a bulk of heterogenous
items. The items must implement the RENDER-AS method. The
calls RENDER-AS with its VIEW slot. Therefore you
have provide an implementation for the keyword supplied by VIEW in
your .
#+LATEX: \\\\
You can limit the displayed items with the MAX-ITEMS-TO-DISPLAY
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 object is rendered,
then selectable page numbers are displayed on the bottom too.
#+LATEX: \\\\
Each item can be accessed. When accessing the item a specific
given function is called with the item as parameter.
#+LATEX: \\\\
The following example covers all functionality:
#+BEGIN_SRC lisp
(defclass ()
((id
:initarg :id
:reader id)
(name
:initarg :name
:reader name)
(description
:initarg :description
:reader description)))
(defmethod render-as ((this ) (view (eql :short)))
(format nil "

id: ~aname: ~adesc: ~a

"
(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 '
: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 '))
(set-widget-for-session
:viewgrid
(make-widget :session '
: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))))
#+END_SRC
*** Forms
Forms can be pretty annoying but with the you don't have
to care for anything but for the naming of the inputs ever again. Each
consists of 0 to n objects. If you have 0
objects it essentially only behaves like a
.
#+LATEX: \\\\
is the base class for fields. Fields can be:
- :: Is basically an abstraction of the HTML input-tag.
- :: Consists of objects.
Of course you can implement your own classes too! But
keep in mind that *the default already implements
constraints*.
#+LATEX: \\\\
To understand how constraints for forms work an examination of the
available slots for 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
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*.
#+LATEX: \\\\
Consider the following example for additional help:
#+BEGIN_SRC lisp
(defvar *password-field*
(make-instance '
: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-type "text"
:name "text"
:value ""
:required t))
(choice-field (make-instance
'
:name "selection"
:options
(list (make-instance '
:value "first")
(make-instance '
:value "second"
:display-value "Other")))))
(make-widget :global '
: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 '))
(setf (body doc)
*form-widget*)))
#+END_SRC
*** 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.
#+LATEX: \\\\
By default the protection is an empty list (therefore NIL), which
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).
#+LATEX: \\\\
The class holds the PROTECTED slot. This slots value
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)
#+BEGIN_SRC lisp
(defvar *specific-protected-widget*
(protect-widget
(make-widget :global '
:text "