Weblocks Tutorial
This assumes you know web programming. It may also assume you
understand continuations. Continuations are used in Weblocks to let
you write a request handler like this:
Imagine wanting to pause request handling to ask a user another
question first:
(defun handle-request (r)
(let ((choice (query-user "blue/red pill?")))
(render "you chose ~A" choice)))
query-user, render are made up functions. query-user hides something
unusual. Mid-way through completion, it pauses execution, sends a
stream of bytes to the web browser containing an html form with a
question, waits until the user answers it, then that http request
causes execution to resume. The pause involves packaging up the
state of execution and storing it away for later retrieval.
----
I'll assume you're an expert programmer, but don't yet know how to
use Weblocks. It took me a while to understand it. That's the time
I hope to save you. I already knew Seaside in Smalltalk well, which
is also continuation based.
I don't use objects. This won't show you automatic persistence of
objects. Its focus is on the web layer.
(ql:quickload '(weblocks))
(use-package '(weblocks))
(defun entry (c)
(setf (composite-widgets c) (list "hallo world")))
(defwebapp tute :init-user-session 'entry)
(start-weblocks)
;go to http://localhost:8080/tute to see "hallo world"
A visitor to your site gets an http session. Weblocks creates a
composite widget representing what they see. Above, #'entry is
called to initialize it. We initialize it to a list with only one
widget. Of type string. Functions are another type of widget:
(defun w1 ()
(with-html
(:h1 "hallo world")))
(defun entry (c)
(setf (composite-widgets c) (list #'w1)))
(reset-sessions)
;reload http://localhost:8080/tute
;change the body of w1
(defun w1 ()
(with-html
(:h1 "hi again")))
;reload web page
;change not reflected
;why not?
Your http session is the same old one. #'entry isn't called on every
request, just when a new http session is set up. Further requests
just render the same composite widget object hierarchy that that
web user already has. Different web users have different instances
of the composite widget hierarchy. That top level component can
change state (independently for each user).
(reset-sessions)
Now refresh to see "hi again". #'entry needed to be called after
we threw away all weblocks's old sessions.
Function widgets like #'w1 above get called every time Weblocks is
rendering a user's composite widget tree. Its body is expected to
write bytes to the http response stream. We use the cl-who library
to help us type our html. As we go along, I'll explain those aspects
of cl-who that you can't guess.
----
Here's a web form:
(use-package 'cl-who)
(defun w1 ()
(with-html-form (:POST
(lambda (&key name &allow-other-keys)
(setf (webapp-session-value "u") name))
:use-ajax-p nil)
(fmt "hallo ~A" (webapp-session-value "u")) (:br)
"what's your name?"
(:input :name "name" :type "text")
(:input :type "submit")))
(reset-sessions)
;refresh web page
The lambda is invoked on form submission. Its key arguments are
named after the form's input fields. The lambda's body here stores
the user's name into a new http session variable on the server
called u, which is initially nil. We turn off ajax to be able to
see updated html after form submission.
---------
do-widget provides the pause/resume feature I described. Below, it
replaces the current component with another. When that one returns,
it swaps the original component back in and resumes it. I use it
in form handlers like the lambda above, and in anchor link handlers.
First a technicality.
The proper way to make widgets from things like functions appears
to be:
(make-widget #'my-function)
not just
#'my-function
even though that's what we've done so far and Weblocks has figured
it out. Here it doesn't figure it out, so I'm going to be explicit.
Okay, to have Weblocks swap widgets as I described, I find I need
to insert one composite widget between the top level one and my own
set. So let's rewrite the entry point:
(defun entry (c)
(setf (composite-widgets c)
(list (make-instance 'composite
:widgets (list (make-widget #'w1))))))
;that innermost list tends to hold all the widgets making up my page.
(reset-sessions)
And now we're set:
(defun w1 ()
(let ((c *current-widget*))
(with-html
"this is the top level" (:br)
(render-link (lambda/cc (&rest noclue) (do-widget c #'sub1))
"click here"
:ajaxp nil))))
(defun sub1 (k)
(with-html
"you are now in the subroutine" (:br)
(render-link (lambda (&rest noclue) (answer k 'anyresult))
"return"
:ajaxp nil)))
;refresh the page
I store the current-widget away. When "click here"'s lambda is
invoked, do-widget is called to replace it with another widget. A
function widget. Function widgets need to take a parameter, k, to
be callable from do-widget. k represents a continuation that can
be resumed later by calling answer.
The user can hang about inside sub1. Refreshing the page will keep
him there. Clicking the "return" link will invoke its handler, which
we've written to replace sub1 with the original w1 via answer. This
also resumes execution of the calling handler right after the code
that calls do-widget. (There is no more code in this case).
;I turn off ajax because ajax is not my focus today
;I don't know why forms want :use-ajax-p and render-links want :ajaxp
Lisp doesn't have continuations. There is a library, cl-cont, that
restructures your code to get the same effect. That's why we use
lambda/cc above. Use it or defun/cc for any function containing a
do-widget. Then that code will be restructured.
Exercises
1. Write a function req-text that displays a textarea. On submission
have it answer the submitted text to the function that called it.
2. Have the calling function repeatedly call req-text until the
answer is non-empty. Then have it display the character count.
3. Ask the user for a number, then another number, and show him the
sum.
--
Send any feedback to vibhu at the domain wispym.com
July 2015