8a7c1bf7 |
1 | #title Roadmap to UCW Codebase |
2 | |
3 | * Abstract |
4 | |
5 | [[http://common-lisp.net/project/ucw/][UnCommon Web]] is a very powerful and mature web framework for Common |
6 | Lisp, but is a bit difficult to learn. It is documented |
7 | extensively--in the form of docstrings. These are extremely helpful |
8 | once you've figured out the rough structure of UCW, but they are of no |
9 | help when first learning unless you just read most of the source. I |
10 | ended up having to do that, and after some urging along by folks in |
11 | =#ucw= I decided to clean up my planner notes and publish them for |
12 | public consumption. |
13 | |
14 | The roadmap is presented with major sections ordered in a logical |
15 | order for learning the framework. The sections are ordered internally |
16 | in order of most immediately useful to least, but it may be worth |
17 | hopping between major sections before reading all of the details. I |
18 | have used abridged class definitions and docstrings with occasional |
19 | commentary to clarify things. |
20 | |
21 | * Roadmap |
22 | |
23 | ** Applications |
24 | |
25 | Applications are a bundle of entry points. The base class is, |
26 | naturally, =standard-application=, but you should instead derive your |
27 | application class from =modular-application= and any standard or custom |
28 | application mixins you find useful. |
29 | |
30 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/rerl/standard-classes.lisp][src/rerl/standard-classes.lisp]] |
31 | |
32 | <src lang="lisp"> |
33 | (defclass standard-application (application) |
34 | ((url-prefix :initarg :url-prefix |
35 | :documentation "A string specifying the |
36 | start (prefix) of all the urls this app should handle. |
37 | |
38 | This value is used by the standard-server to decide what app a |
39 | particular request is aimed at and for generating links to |
40 | actions within the app. ") |
41 | (www-roots :initarg :www-roots |
42 | :documentation "A list of directories (pathname |
43 | specifiers) or cons-cell (URL-subdir . pathname) to use when looking for static files.") |
44 | (dispatchers :initarg :dispatchers |
45 | :documentation "A list of request |
46 | dispatchers. The user supplied list of dispatchers is extended |
47 | with other dispatchers that are required for UCW to function |
48 | properly (action-dispatcher, a parenscript-dispatcher, etc). If |
49 | you want full control over the active dispatchers use the (setf |
50 | application.dispatchers) accessor or, if you want control over |
51 | the order of the dispathcers, (slot-value instance |
52 | 'dispatchers).")) |
53 | (:documentation "The default UCW application class.")) |
54 | </src> |
55 | |
56 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/rerl/modular-application/modular-application.lisp][src/rerl/modular-application/modular-application.lisp]] |
57 | |
58 | <src lang="lisp"> |
59 | (defclass modular-application-mixin () |
60 | () |
61 | (:documentation "Superclass for all application mixins.")) |
62 | |
63 | (defclass modular-application (standard-application modular-application-mixin) |
64 | ...) |
65 | </src> |
66 | |
67 | *** Cookie |
68 | |
69 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/rerl/modular-application/cookie-module.lisp][src/rerl/modular-application/cookie-module.lisp]] |
70 | |
71 | <src lang="lisp"> |
72 | (defclass cookie-session-application-module (modular-application-mixin) |
73 | (:documentation "Class for applications which use cookies for sesion tracking. |
74 | |
75 | Cookie session applications work exactly like |
76 | standard-applications except that when the session is not found |
77 | using the standard mechanisms the id is looked for in a cookie.")) |
78 | </src> |
79 | |
80 | This is the most useful of the application components. It makes your |
81 | application urls readable by stashing the session id into a cookie |
82 | rather than as a set of long and ugly GET parameters. |
83 | |
84 | *** L10n |
85 | |
86 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/rerl/modular-application/l10n-module.lisp][src/rerl/modular-application/l10n-module.lisp]] |
87 | |
88 | <src lang="lisp"> |
89 | (defclass l10n-application-module (modular-application-mixin) |
90 | (:documentation "Application class which can handle l10n requests.")) |
91 | </src> |
92 | |
93 | *** Secure |
94 | |
95 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/rerl/modular-application/security-module.lisp][src/rerl/modular-application/security-module.lisp]] |
96 | |
97 | <src lang="lisp"> |
98 | (defclass secure-application-module (modular-application-mixin) |
99 | (:documentation |
100 | "Mixin class for applications which require authorized access. |
101 | Concrete application must specialize the following methods: |
102 | APPLICATION-FIND-USER (APPLICATION USERNAME) |
103 | APPLICATION-CHECK-PASSWORD (APPLICATION USER PASSWORD) |
104 | APPLICATION-AUTHORIZE-CALLZE-CALL (APPLICATION USER FROM-COMPONENT TO-COMPONENT).")) |
105 | </src> |
106 | |
107 | ** Components |
108 | |
109 | A component is a special class that handles the complexities of |
110 | continuation suspension and such for you. New components are derived |
111 | from the existing ones by using =defcomponent= instead of =defclass=. This |
112 | adds a few extra slot and class options, and ensures that the proper |
113 | metaclass is set. |
114 | |
115 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/rerl/standard-component/standard-component.lisp][src/rerl/standard-component/standard-component.lisp]] |
116 | |
117 | <src lang="lisp"> |
118 | (defmacro defcomponent (name supers slots &rest options) |
119 | "Macro for defining a component class. |
120 | |
121 | This macro is used to create component classes and provides |
122 | options for easily creating the methods which often accompany a |
123 | component definition. |
124 | |
125 | NAME, SUPERS and SLOTS as treated as per defclass. The following |
126 | extra options are allowed: |
127 | |
128 | (:ENTRY-POINT url (&key application class)) - Define an |
129 | entry-point on the url URL which simply calls an instance of |
130 | this component. Any request parameters passed to the entry-point |
131 | are used to initialize the slots in the component. This option |
132 | may appear multiple times. |
133 | |
134 | (:DEFAULT-BACKTRACK function-designator) - Unless the slots |
135 | already have a :backtrack option FUNCTION-DESIGNATOR is |
136 | added. As with the 'regular' :backtrack options if you pass T |
137 | here it is assumed to mean #'IDENTITY. |
138 | |
139 | (:RENDER (&optional COMPONENT) &body BODY) - Generate a render |
140 | method specialized to COMPONENT. COMPONENT may be a symbol, in |
141 | which case the method will be specialized on the componnet |
142 | class. If COMPONNET is omited the component is bound to a |
143 | variable with the same name as the class. |
144 | |
145 | (:ACTION &optional NAME) - Generate a defaction form named |
146 | NAME (which defaults to the name of the component) which simply |
147 | CALL's this component class passing all the arguments passed to |
148 | the action as initargs.") |
149 | |
150 | ;;; Extra Slot Options |
151 | "Other than the initargs for standard slots the following |
152 | options can be passed to component slots: |
153 | |
154 | :backtrack [ T | NIL | FUNCTION-NAME ] - Specify that this slot |
155 | should be backtracked (or not if NIL is passed as the value). If |
156 | the value is neither T nor NIL then it must be a function which |
157 | will be used as the copyer. |
158 | |
159 | :component [ TYPE | ( TYPE &rest INITARGS ) ] - Specify that this |
160 | slot is actually a nested component of type TYPE. When instances |
161 | of the class are created this slot will be set to an instance of |
162 | type TYPE and it's place will be set to this slot. If a list is |
163 | passed to :component then TYPE (which isn't evaluated) will be |
164 | passed as the first argument to make-instance. The INITARGS will |
165 | be eval'd and apply'd to make-instance. The result of this call |
166 | to make-instance will be used as the effective component |
167 | object." |
168 | </src> |
169 | |
170 | *** Windows |
171 | |
172 | A window-component represents a top level browser window, naturally. |
173 | |
174 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/window.lisp][src/components/window.lisp]] |
175 | |
176 | <src lang="lisp"> |
177 | (defclass window-component () |
178 | ((content-type))) |
179 | |
180 | (defclass simple-window-component (window-component) |
181 | ((title) |
182 | (stylesheet) |
183 | (javascript :documentation "List of javascript includes. |
184 | |
185 | Each element must be a list whose first value is either the |
186 | symbol :SRC or :JS. |
187 | |
188 | (:SRC url) - writes <script src=\"URL\"></script> tag. |
189 | (:JS form) - equivalent to (:SCRIPT (js:js* form)) |
190 | (:SCRIPT string) - write <script>STRING</script>. |
191 | |
192 | The elements will be rendered in order.") |
193 | ...)) |
194 | </src> |
195 | |
196 | =window-component= could be useful for doing things like dumping binary |
197 | data to the user, or just deriving your own funky top level window |
198 | type. |
199 | |
200 | =simple-window-component= is the easiest for displaying standard |
201 | webpage. It provides a wrapping method on render that displays the |
202 | html boilerplate based on your component slot values which is what one |
203 | wants most of the time. The initargs to =simple-window-component= have |
204 | the same names as the slots. |
205 | |
206 | **** Status Bar |
207 | |
208 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/status-bar.lisp][src/components/status-bar.lisp]] |
209 | |
210 | There is a generic status bar interface. Messages severity is one of |
211 | =(:error :warn :info)=. Note that the default status bar render method |
212 | just shows a div with status messages. A derivative could be defined |
213 | to insert messages into the browser status bar. |
214 | |
215 | <src lang="lisp"> |
216 | (defcomponent status-bar () |
217 | ((messages :documentation "An ALIST of the messages to |
218 | show. Each element is a cons of the form (SEVERITY . |
219 | MESSAGE). SEVERITY is one of :ERROR, :WARN, :INFO and MESSAGE is |
220 | a string which will be html-escaped.") |
221 | ...) |
222 | (:documentation "Stateless status bar to display messages.")) |
223 | |
224 | (defgeneric add-message (status-bar msg &key severity &allow-other-keys) |
225 | (:documentation "Add the message text MSG to STATUS-BAR with |
226 | severity SEVERITY.")) |
227 | </src> |
228 | |
229 | <src lang="lisp"> |
230 | (defcomponent status-bar-mixin () |
231 | ((status-bar :accessor status-bar |
232 | :initarg status-bar |
233 | :component (status-bar)))) |
234 | |
235 | (defmethod show-status-bar ((win status-bar-mixin)) |
236 | (render (status-bar win))) |
237 | |
238 | (defgeneric show-message (msg &key severity &allow-other-keys) |
239 | (:documentation "Show a message in the status bar. Only works if |
240 | current window is a status-bar-mixin")) |
241 | </src> |
242 | |
243 | **** Redirect |
244 | |
245 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/redirect.lisp][src/components/redirect.lisp]] |
246 | |
247 | <src lang="lisp"> |
248 | (defclass redirect-component () |
249 | ((target :accessor target :initarg :target)) |
250 | (:metaclass standard-component-class) |
251 | (:documentation "Send a client redirect. |
252 | |
253 | This component, which must be used as a window-component, |
254 | redirects the client to the url specified in the target slot. A |
255 | 302 (as opposed to 303) response code is sent to ensure |
256 | compatability with older browsers. |
257 | |
258 | The redirect component never answers.")) |
259 | </src> |
260 | |
261 | There is also a =meta-refresh= procedure. |
262 | |
263 | <src lang="lisp"> |
264 | (defun/cc meta-refresh () |
265 | "Cause a meta-refresh (a freshly got (GET) url) at this point. |
266 | This is useful in order to have a GET url after a form POST's |
267 | actions have completed running. The user can then refresh to his |
268 | heart's content.") |
269 | </src> |
270 | |
271 | *** Containers |
272 | |
273 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/container.lisp][src/components/container.lisp]] |
274 | |
275 | <src lang="lisp"> |
276 | (defclass container () |
277 | (...) |
278 | (:metaclass standard-component-class) |
279 | (:documentation "Allow multiple components to share the same place. |
280 | |
281 | The container component serves to manage a set of components. |
282 | It does not provide any render impementation, which is the |
283 | resposibility of the subclasses (e.g. switching-container or |
284 | list-container). |
285 | |
286 | Each contained component has a \"key\" associated with it which |
287 | is used to retrieve a particular component. Keys are compared with |
288 | container.key-test. |
289 | |
290 | The :contents inintarg, if provided, must be either a list of (key . |
291 | component) or a list of components. In the latter case it will |
292 | be converted into (component . component) form.")) |
293 | </src> |
294 | |
295 | **** Protocol |
296 | |
297 | - =child-components= |
298 | - =find-component CONTAINER KEY= |
299 | - =remove-component= |
300 | - =(setf find-component CONTAINER KEY) COMPONENT= -> |
301 | =add-component CONTAINER COMPONENT KEY= |
302 | |
303 | **** Switching Container |
304 | |
305 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/container.lisp][src/components/container.lisp]] |
306 | |
307 | <src lang="lisp"> |
308 | (defclass switching-container ... |
309 | (:documentation "A simple renderable container component. |
310 | |
311 | This component is like the regular CONTAINER but serves to manage a set |
312 | of components which share the same place in the UI. Therefore it provides |
313 | an implementation of RENDER which simply renders its current component. |
314 | |
315 | The switching-container component class is generally used as the super |
316 | class for navigatation components and tabbed-pane like |
317 | components.")) |
318 | </src> |
319 | |
320 | Subclass and =(defmethod render :around ...)= to render navigation using |
321 | =(call-next-method)= to render the selected component. |
322 | |
323 | ***** Protocol |
324 | |
325 | - =container.current-component COMPONENT= |
326 | - =(setf container.current-component CONTAINER) COMPONENT= |
327 | |
328 | **** Tabbed Pane |
329 | |
330 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/tabbed-pane.lisp][src/components/tabbed-pane.lisp]] |
331 | |
332 | <src lang="lisp"> |
333 | (defcomponent tabbed-pane (switching-container) |
334 | (:documentation "Component for providing the user with a standard \"tabbed pane\" GUI widget.")) |
335 | </src> |
336 | |
337 | Provides a generic tabbed pane that renders a nested div split into a |
338 | naviation and content box. The navigation box is a set of styled divs |
339 | containing the navigation links. |
340 | |
341 | *** Dialogs |
342 | |
343 | A few convenience dialogs are provided for grabbing data from the |
344 | user. |
345 | |
346 | **** login |
347 | |
348 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/login.lisp][src/components/login.lisp]] |
349 | |
350 | <src lang="lisp"> |
351 | (defclass login () |
352 | ((username) (password) (message)) |
353 | (:documentation "Generic login (input username and password) component. |
354 | |
355 | This component, which must be embedded in another component, |
356 | presents the user with a simple two fielded login form. |
357 | |
358 | When the user attempts a login the action try-login is called, |
359 | try-login calls the generic function check-credentials passing it |
360 | the login component. If check-credentials returns true then the |
361 | login-successful action is called, otherwise the message slot of |
362 | the login component is set (to a generic \"bad username\" |
363 | message). |
364 | |
365 | The default implementaion of login-successful simply answers t, |
366 | no default implementation of check-credentials is |
367 | provided. Developers should use sub-classes of login for which |
368 | all the required methods have been definined.") |
369 | (:metaclass standard-component-class)) |
370 | </src> |
371 | |
372 | <src lang="lisp"> |
373 | (defgeneric check-credentials (login) |
374 | (:documentation "Returns T if LOGIN is valid.")) |
375 | |
376 | (defaction login-successful ((l login)) |
377 | (answer t)) |
378 | </src> |
379 | |
380 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/user-login.lisp][src/components/user-login.lisp]] |
381 | |
382 | <src lang="lisp"> |
383 | (defcomponent user-login (simple-window-component status-bar-mixin) |
384 | ((username string-field) (password password-field))) |
385 | </src> |
386 | |
387 | Used by =secure-application-module= to provide a user login. Relevant |
388 | protocol details follow. |
389 | |
390 | <src lang="lisp"> |
391 | (defmethod check-credentials ((self user-login)) |
392 | (let* ((username (value (username self))) |
393 | (password (value (password self))) |
394 | (user (find-application-user username))) |
395 | (when (and user (check-user-password user password)) |
396 | user))) |
397 | |
398 | (defgeneric application-find-user (application username) |
399 | (:documentation "Find USER by USERNAME for APPLICATION.")) |
400 | </src> |
401 | |
402 | **** error |
403 | |
404 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/error.lisp][src/components/error.lisp]] |
405 | |
406 | <src lang="lisp"> |
407 | (defclass error-message (simple-window-component) |
408 | ((message :accessor message :initarg :message :initform "ERROR [no message specified]")) |
409 | (:documentation "Generic component for showing server side |
410 | error messages.") |
411 | (:metaclass standard-component-class)) |
412 | |
413 | (defclass error-component (error-message) |
414 | ((condition :accessor error.condition :initarg :condition :initform nil) |
415 | (backtrace :accessor error.backtrace :initarg :backtrace)) |
416 | (:documentation "Generic component for showing server side |
417 | error conditions. Unlike ERROR-MESSAGE this component also |
418 | attempts to display a backtrace.") |
419 | (:metaclass standard-component-class)) |
420 | </src> |
421 | |
422 | **** message |
423 | |
424 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/message.lisp][src/components/message.lisp]] |
425 | |
426 | <src lang="lisp"> |
427 | (defclass info-message () |
428 | ((message :initarg :message :accessor message) |
429 | (ok-text :initarg :ok-text :accessor ok-text :initform "Ok.")) |
430 | (:documentation "Component for showing a message to the user. |
431 | |
432 | If the OK-TEXT slot is non-NIL component will use that as the |
433 | text for a link which, when clicked, causes the component to |
434 | answer. It follows that if OK-TEXT is NIL this component will |
435 | never answer.") |
436 | (:metaclass standard-component-class)) |
437 | </src> |
438 | |
439 | **** option-dialog |
440 | |
441 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/option-dialog.lisp][src/components/option-dialog.lisp]] |
442 | |
443 | <src lang="lisp"> |
444 | (defclass option-dialog (template-component) |
445 | ((message) (options) (confirm)) |
446 | (:default-initargs :template-name "ucw/option-dialog.tal") |
447 | (:documentation "Component for querying the user. |
448 | |
449 | The value of the slot MESSAGE is used as a general heading. |
450 | |
451 | The OPTIONS slot must be an alist of (VALUE . LABEL). LABEL (a |
452 | string) will be used as the text of a link which, when clikced, |
453 | will answer VALUE. |
454 | |
455 | If the CONFIRM slot is T the user will be presented with a second |
456 | OPTION-DIALOG asking the user if they are sure they want to |
457 | submit that value.") |
458 | (:metaclass standard-component-class)) |
459 | </src> |
460 | |
461 | A macro to present an option dialog is provided. |
462 | |
463 | <src lang="lisp"> |
464 | (defmacro option-dialog ((message-spec &rest message-args) &body options) |
465 | ...) |
466 | </src> |
467 | |
468 | =message-spec= is passed to =format= if =message-args= are supplied, and |
469 | used as a string literal otherwise. This does not provide a way to set |
470 | the confirm property which makes the macro not so generally useful. |
471 | |
472 | *** Forms |
473 | |
474 | Reasonably useful forms library that integrates easily with TAL. |
475 | |
476 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/form.lisp][src/components/form.lisp]] |
477 | |
478 | <src lang="lisp"> |
479 | (defclass form-field () |
480 | ((validators :documentation "List of validators which will be |
481 | applied to this field.") |
482 | (initially-validate :documentation "When non-NIL the |
483 | validators will be run as soon as the page |
484 | is rendered."))) |
485 | |
486 | (defgeneric value (form-field) |
487 | (:documentation "The lispish translated value that represents the form-field.")) |
488 | |
489 | (defgeneric (setf value) (new-value form-field) |
490 | (:documentation "Set the value of a form-field with translation to client.")) |
491 | |
492 | (defclass generic-html-input (form-field html-element) |
493 | ((client-value :accessor client-value :initarg :client-value |
494 | :initform "" |
495 | :documentation "The string the client submitted along with this field.") |
496 | (name :accessor name :initarg :name :initform nil) |
497 | (accesskey :accessor accesskey :initarg :accesskey :initform nil) |
498 | (tooltip :accessor tooltip :initarg :tooltip :initform nil) |
499 | (tabindex :accessor tabindex :initarg :tabindex :initform nil)) |
500 | (:default-initargs :dom-id (js:gen-js-name-string :prefix "_ucw_"))) |
501 | </src> |
502 | |
503 | Fields are rendered into the extended =<ucw:input= yaclml tag which |
504 | supports a few fancy features. The =:accessor= for all form elements is |
505 | set to =(client-value FIELD)=, and you should use =value= to access the |
506 | Lisp value associated with it. |
507 | |
508 | <src lang="lisp"> |
509 | (deftag-macro <ucw:input (&attribute accessor action reader writer name id (default nil) |
510 | &allow-other-attributes others) |
511 | "Generic INPUT tag replacement. |
512 | |
513 | If the ACCESSOR attribute is specified then it must be a PLACE |
514 | and it's value will be used to fill the input, when the form is |
515 | submitted it will be set to the new value. |
516 | |
517 | If ACTION is specefied then when the form is submitted via this |
518 | input type=\"submit\" tag the form will be eval'd. when the |
519 | submit (or image) is clicked. DEFAULT means that the ACTION |
520 | provided for this input tag will be the default action of the |
521 | form when pressing enter in a form field. If more then one, then |
522 | the latest wins.") |
523 | </src> |
524 | |
525 | Validation of form fields are supported by adding to the validators |
526 | list. |
527 | |
528 | <src lang="lisp"> |
529 | (defclass validator () |
530 | ((message :accessor message :initarg :message :initform nil))) |
531 | |
532 | (defgeneric validate (field validator) |
533 | (:documentation "Validate a form-field with a validator.")) |
534 | |
535 | (defgeneric javascript-check (field validator) |
536 | (:documentation "Generate javascript code for checking FIELD against VALIDATOR. |
537 | |
538 | This is the convenience entry point to generate-javascript-check, |
539 | methods defined on this generic funcition should return a list of |
540 | javascript code (as per parenscript) which tests against the |
541 | javascript variable value.")) |
542 | |
543 | (defgeneric javascript-invalid-handler (field validator) |
544 | (:documentation "The javascript code body for when a field is invalid.")) |
545 | |
546 | (defgeneric javascript-valid-handler (field validator) |
547 | (:documentation "Generate the javascript body for when a field is valid.")) |
548 | </src> |
549 | |
550 | **** Standard Form Fields |
551 | |
552 | <src lang="lisp"> |
553 | (defclass string-field (generic-html-input) |
554 | ((input-size) (maxlength))) |
555 | |
556 | (defclass password-field (string-field)) |
557 | (defclass number-field (string-field)) |
558 | (defclass integer-field (number-field)) |
559 | |
560 | (defclass in-field-string-field (string-field) |
561 | ((in-field-label :documentation "This slot, if non-NIL, will be |
562 | used as an initial field label. An initial |
563 | field label is a block of text which is placed |
564 | inside the input element and removed as soon |
565 | as the user edits the field. Obviously this |
566 | field is overidden by an initial :client-value |
567 | argument."))) |
568 | |
569 | (defclass textarea-field (generic-html-input) |
570 | ((rows) (cols))) |
571 | |
572 | (defclass date-field (form-field widget-component) |
573 | ((year) (month) (day))) |
574 | |
575 | (defclass dmy-date-field (date-field) |
576 | (:documentation "Date fields which orders the inputs day/month/year")) |
577 | (defclass mdy-date-field (date-field)) |
578 | |
579 | (defclass select-field (generic-html-input) |
580 | ((data-set :documentation "The values this select chooses |
581 | from.")) |
582 | (:documentation "Form field used for selecting one value from a |
583 | list of available options.")) |
584 | |
585 | (defgeneric render-value (select-field value) |
586 | (:documentation "This function will be passed each value in the field's |
587 | data-set and must produce the body of the corresponding |
588 | <ucw:option tag.")) |
589 | |
590 | (defclass mapping-select-field (select-field) |
591 | (:documentation "Class used when we want to chose the values of |
592 | a certain mapping based on the keys. We render the keys in the |
593 | select and return the corresponding value from the VALUE |
594 | method.")) |
595 | |
596 | (defclass hash-table-select-field (mapping-select-field)) |
597 | (defclass alist-select-field (mapping-select-field)) |
598 | (defclass plist-select-field (mapping-select-field)) |
599 | |
600 | (defclass radio-group (generic-html-input) |
601 | ((value-widgets))) |
602 | |
603 | (defclass radio-button (generic-html-input) |
604 | ((value) |
605 | (group :documentation "The RADIO-GROUP this button is a part |
606 | of.")) |
607 | (:documentation "A widget representing a single radio |
608 | button. Should be used in conjunction with a RADIO-GROUP.")) |
609 | |
610 | (defmethod add-value ((group radio-group) value) |
611 | "Adds radio-button with value to group") |
612 | |
613 | (defclass checkbox-field (generic-html-input)) |
614 | (defclass file-upload-field (generic-html-input)) |
615 | (defclass submit-button (generic-html-input) |
616 | ((label))) |
617 | </src> |
618 | |
619 | ***** File Upload Field |
620 | |
621 | Calling =value= on a =file-upload-field= returns a mime encoded body |
622 | part. =(mime-part-body (value FIELD))= will return a **binary stream** |
623 | attached to the contents of the file. The =Content-Type= header should |
624 | be set to the MIME type of the file being uploaded. |
625 | |
626 | <src lang="lisp"> |
627 | (defgeneric mime-part-headers (mime-part) |
628 | (:documentation "Returns an alist of the headers of MIME-PART. |
629 | |
630 | The alist must be of the form (NAME . VALUE) where both NAME and |
631 | VALUE are strings.")) |
632 | |
633 | (defgeneric mime-part-body (mime-part) |
634 | (:documentation "Returns the body of MIME-PART.")) |
635 | </src> |
636 | |
637 | **** Standard Validators |
638 | |
639 | <src lang="lisp"> |
640 | (defclass not-empty-validator (validator)) |
641 | |
642 | (defclass value-validator (validator) |
643 | (:documentation "Validators that should only be applied if there is a value. |
644 | That is, they always succeed on nil.")) |
645 | |
646 | (defclass length-validator (value-validator) |
647 | ((min-length :accessor min-length :initarg :min-length |
648 | :initform nil) |
649 | (max-length :accessor max-length :initarg :max-length |
650 | :initform nil))) |
651 | |
652 | (defclass string=-validator (validator) |
653 | ((other-field :accessor other-field :initarg :other-field)) |
654 | (:documentation "Ensures that a field is string= to another one.")) |
655 | |
656 | (defclass regex-validator (value-validator) |
657 | ((regex :accessor regex :initarg :regex :initform nil))) |
658 | |
659 | (defclass e-mail-address-validator (regex-validator)) |
660 | |
661 | (defclass phone-number-validator (regex-validator)) |
662 | |
663 | (defclass is-a-number-validator (value-validator)) |
664 | (defclass is-an-integer-validator (is-a-number-validator)) |
665 | |
666 | (defclass number-range-validator (is-a-number-validator) |
667 | ((min-value :accessor min-value :initarg :min-value :initform nil) |
668 | (max-value :accessor max-value :initarg :max-value :initform nil))) |
669 | </src> |
670 | |
671 | **** Simple Form Helper |
672 | |
673 | UCW provides a helper class for developing forms. Subclass and add the |
674 | elements you wish to include in the form. A =:wrapping= method renders |
675 | the form boilerplate and then calls your =render=. |
676 | |
677 | <src lang="lisp"> |
678 | (defcomponent simple-form (html-element) |
679 | ((submit-method :accessor submit-method |
680 | :initform "post" |
681 | :initarg :submit-method) |
682 | (dom-id :accessor dom-id |
683 | :initform (js:gen-js-name-string :prefix "_ucw_simple_form_") |
684 | :initarg :dom-id)) |
685 | (:default-initargs :dom-id "ucw-simple-form")) |
686 | </src> |
687 | |
688 | *** Templates |
689 | |
690 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/template.lisp][src/components/template.lisp]] |
691 | |
692 | Infrastructure for loading TAL templates as a view of a component. |
693 | |
694 | <src lang="lisp"> |
695 | (defclass template-component (component)) |
696 | (defcomponent simple-template-component (template-component) |
697 | ((environment :initarg :environment :initform nil))) |
698 | |
699 | (defgeneric template-component-environment (component) |
700 | (:documentation "Create the TAL environment for rendering COMPONENT's template. |
701 | |
702 | Methods defined on this generic function must return a TAL |
703 | environment: a list of TAL binding sets (see the documentation |
704 | for YACLML:MAKE-STANDARD-ENVIRONMENT for details on TAL |
705 | environments.)") |
706 | (:method-combination nconc)) |
707 | |
708 | (defmethod template-component-environment nconc ((component template-component)) |
709 | "Create the basic TAL environment. |
710 | |
711 | Binds the symbol ucw:component to the component object itself, |
712 | also puts the object COMPONENT on the environment (after the |
713 | binding of ucw:component) so that slots are, by default, |
714 | visable." |
715 | (make-standard-environment `((component . ,component)) component)) |
716 | |
717 | (defmethod render ((component template-component)) |
718 | "Render a template based component. |
719 | |
720 | Calls the component's template. The name of the template is the |
721 | value returned by the generic function |
722 | template-component.template-name, the template will be rendered |
723 | in the environment returned by the generic function |
724 | template-component-environment." |
725 | (render-template *context* |
726 | (template-component.template-name component) |
727 | (template-component-environment component))) |
728 | |
729 | </src> |
730 | |
731 | Subclass and override methods. =simple-template-component= only provides |
732 | the ability to set environment variables in initarg. Subclass to |
733 | provide automagic template file name generation and such. |
734 | |
735 | *** Utility Mixin Components |
736 | |
737 | **** Range View |
738 | |
739 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/range-view.lisp][src/components/range-view.lisp]] |
740 | |
741 | <src lang="lisp"> |
742 | (defclass range-view (template-component) |
743 | (:default-initargs :template-name "ucw/range-view.tal") |
744 | (:documentation "Component for showing the user a set of data one \"window\" at a time. |
745 | |
746 | The data set is presented one \"window\" at a time with links to |
747 | the the first, previous, next and last window. Each window shows |
748 | at most WINDOW-SIZE elements of the data. The data is passed to |
749 | the range-view at instance creation time via the :DATA initarg. |
750 | |
751 | The generic function RENDER-RANGE-VIEW-ITEM is used to render |
752 | each item of DATA. |
753 | |
754 | In order to change the rendering of the single elements of a |
755 | range view developer's should create a sub class of RANGE-VIEW |
756 | and define their RENDER-RANGE-VIEW-ITEM methods on that.") |
757 | (:metaclass standard-component-class)) |
758 | </src> |
759 | |
760 | <src lang="lisp"> |
761 | (defgeneric render-range-view-item (range-view item) |
762 | (:documentation "Render a single element of a range-view.") |
763 | (:method ((range-view range-view) (item t)) |
764 | "Standard implementation of RENDER-RANGE-VIEW-ITEM. Simply |
765 | applies ITEM to princ (via <:as-html)." |
766 | (declare (ignore range-view)) |
767 | (<:as-html item))) |
768 | </src> |
769 | |
770 | **** Widget |
771 | |
772 | Mixin with existing component to wrap in a div or span. This is handy |
773 | for defining lightweight widgets embedded within other components. |
774 | |
775 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/html-element.lisp][src/components/html-element.lisp]] |
776 | |
777 | <src lang="lisp"> |
778 | (defclass html-element (component) |
779 | ((css-class) |
780 | (dom-id) |
781 | (css-style) |
782 | (extra-tags) |
783 | (events)) |
784 | (:documentation "An HTML element. |
785 | |
786 | HTML elements control aspects that are relevant to almost all tags. |
787 | |
788 | Firstly they provide a place to store the class, id, and style of the |
789 | component. The specific render methods of the components themselves |
790 | must pass these values to whatever code is used to render the actual |
791 | tag. |
792 | |
793 | Secondly, they allow javascript event handlers to be registered for a |
794 | tag. The events slot can be filled with a list of lists in the form |
795 | |
796 | (event parenscript-statement*) |
797 | |
798 | For example (\"onclick\" (alert \"You clicked!\") (return nil)). If |
799 | the element has a dom-id, these event handlers are automatically |
800 | added.")) |
801 | </src> |
802 | |
803 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/widget.lisp][src/components/widget.lisp]] |
804 | |
805 | <src lang="lisp"> |
806 | (defclass widget-component (html-element) |
807 | () |
808 | (:documentation "A widget which should be wrapped in a <div>.")) |
809 | |
810 | (defclass inline-widget-component (html-element) |
811 | () |
812 | (:documentation "A widget which should be wrapped in <span> and not <div>")) |
813 | |
814 | (defmethod render :wrap-around ((widget widget-component))) |
815 | (defmethod render :wrap-around ((widget inline-widget-component))) |
816 | </src> |
817 | |
818 | **** Transactions |
819 | |
820 | A mixin to provide transactions. =(open-transaction component)= and |
821 | =(close-transaction component)= open and closed nested |
822 | transactions. After a transaction has been closed an attempt to |
823 | backtrack into a step inside the transaction will result in jumping up |
824 | one level of transactions (or out of the transaction entirely if at |
825 | the top level). This ensures that the transaction is only run once, |
826 | naturally. |
827 | |
828 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/transaction-mixin.lisp][src/components/transaction-mixin.lisp]] |
829 | |
830 | <src lang="lisp"> |
831 | (defcomponent transaction-mixin () |
832 | (...)) |
833 | |
834 | (defmethod/cc open-transaction ((comp transaction-mixin))) |
835 | (defmethod/cc close-transaction ((comp transaction-mixin))) |
836 | </src> |
837 | |
838 | **** Task |
839 | |
840 | =(defaction start ...)= on subclass to run a series of actions bundled |
841 | into a task. |
842 | |
843 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/task.lisp][src/components/task.lisp]] |
844 | |
845 | <src lang="lisp"> |
846 | (defclass task-component (standard-component) |
847 | (...) |
848 | (:documentation "A controller for a single task or operation to |
849 | be performed by the user. |
850 | |
851 | A task component's START action is called as soon as the |
852 | component is instantiated. Task components do not have their own |
853 | RENDER method, in fact they have no graphical representation but |
854 | serve only to order a sequence of other components.")) |
855 | |
856 | (defgeneric/cc start (task) |
857 | (:documentation "action which gets called automatically when |
858 | task-component is active. Use defaction to define your own |
859 | \"start\" action")) |
860 | </src> |
861 | |
862 | **** Cached |
863 | |
864 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/components/cached.lisp][src/components/cached.lisp]] |
865 | |
866 | <src lang="lisp"> |
867 | (defcomponent cached-component () |
868 | ((cached-output :accessor cached-output :initform nil |
869 | :documentation "A string holding the output to |
870 | use for this component. This string will be |
871 | written directly to the html stream and is |
872 | changed by the REFRESH-COMPONENT-OUTPUT |
873 | method." ) |
874 | (timeout :accessor timeout :initarg :timeout |
875 | :documentation "An value specifying how often this |
876 | component needs to be refreshed. The exact |
877 | interpretation of the value depends on the type of |
878 | caching used class.")) |
879 | (:documentation "Component which caches its output. |
880 | |
881 | The component caching API is built around the generic functions |
882 | COMPONENT-DIRTY-P and REFRESH-COMPONENT-OUTPUT and a method on |
883 | RENDER, see the respective docstrings for more details. |
884 | |
885 | Do not use CACHED-COMPONENT directly, use one its subclasses.")) |
886 | |
887 | (defgeneric component-dirty-p (component) |
888 | (:documentation "Returns T is COMPONENT's cache is invalid.")) |
889 | |
890 | (defgeneric update-cache (component) |
891 | (:documentation "Update COMPONENT's cache variables after a refresh.")) |
892 | |
893 | (defcomponent timeout-cache-component (cached-component) |
894 | ((last-refresh :accessor last-refresh :initform nil |
895 | :documentation "The time, exrpessed as a |
896 | universal time, when the component was last rendered.")) |
897 | (:default-initargs |
898 | :timeout (* 30 60 60)) |
899 | (:documentation "Render the component at most every TIMEOUT seconds.")) |
900 | |
901 | (defcomponent num-hits-cache-component (cached-component) |
902 | ((hits-since-refresh :accessor hits-since-refresh |
903 | :initform nil |
904 | :documentation "Number of views since last refresh.")) |
905 | (:default-initargs :timeout 10) |
906 | (:documentation "Render the component every TIMEOUT views.")) |
907 | </src> |
908 | |
909 | Subclass and override =component-dirty-p= to do something useful |
910 | (e.g. flip mark bit when object being presented changes). |
911 | |
912 | ** Control Flow |
913 | |
914 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/rerl/standard-component/control-flow.lisp][src/rerl/standard-component/control-flow.lisp]] |
915 | |
916 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/rerl/standard-action.lisp][src/rerl/standard-action.lisp]] |
917 | |
918 | *** Calling |
919 | |
920 | Most of what you do in UCW will be calling components so this is a bit |
921 | important. Note that calling interrupts the current control flow so if |
922 | you want to render a component in place as part of another component |
923 | just call =render= on it instead. |
924 | |
925 | <src lang="lisp"> |
926 | (defmacro call (component-type &rest component-init-args) |
927 | "Stop the execution of the current action and pass control to |
928 | a freshly created component of type COMPONENT-TYPE. |
929 | |
930 | COMPONENT-INIT-ARGS are passed directly to the underlying |
931 | make-instance call. This form will return if and when the call'd |
932 | component calls answer, the value returned by this form is |
933 | whatever the call'd component passed to answer. |
934 | |
935 | Notes: |
936 | |
937 | This macro assumes that the lexcial variable UCW:SELF is bound to |
938 | the calling component.") |
939 | |
940 | (answer VAL) ; answer parent component ONLY IN ACTIONS |
941 | |
942 | (ok SELF VAL) ; Used to answer a component anywhere and what answer |
943 | ; expands into |
944 | |
945 | (jump COMPONENT-NAME &REST ARGS) ; is similar to call, but replaces |
946 | ; the current component with the new |
947 | ; one and drops any backtracks (back |
948 | ; button will no longer work) |
949 | </src> |
950 | |
951 | =(call COMPONENT-NAME &ARGS INIT-ARGS)= calls =COMPONENT-NAME= and returns |
952 | the value returned by =(ok SELF RETURN-VALUE)= called from within |
953 | =COMPONENT-NAME= |
954 | |
955 | *** Actions |
956 | |
957 | Actions are methods on components. The first argument **must** be a |
958 | component for most of UCW to work. |
959 | |
960 | <src lang="lisp"> |
961 | (defaction NAME (first ...) ...) |
962 | ; (roughly) expands into |
963 | (defmethod/cc NAME (first ...) |
964 | (let ((self first)) |
965 | ...)) |
966 | </src> |
967 | |
968 | =Self= being bound in the current lexical environment is required for |
969 | most UCW control flow things to work. =defaction= hides this from you, |
970 | and was a big source of confusion for me early on (mostly "hmm, why is |
971 | this not working ... where did that come from in the |
972 | macroexpansion!"). |
973 | |
974 | *** Entry Points |
975 | |
976 | <src lang="lisp"> |
977 | (defentry-point url (:application APPLICATION |
978 | :class DISPATCHER-CLASS) |
979 | (PARAM1 ... PARAMN) ; GET / POST vars, bound in body |
980 | body) |
981 | </src> |
982 | |
983 | An entry point is what it sounds like: a static URL matched using the |
984 | mater of =DISPATCHER-CLASS= that enters into =APPLICATION= running the |
985 | code in =body=. An example from a test program I have written |
986 | follows. The entry point allows files to be streamed to user when the |
987 | url audio.ucw?file=FOO is used. |
988 | |
989 | <src lang="lisp"> |
990 | (defentry-point "^(audio.ucw|)$" (:application *golf-test-app* |
991 | :class regexp-dispatcher) |
992 | (file) |
993 | (call 'audio-file-window |
994 | :audio-file (make-instance 'audio-file |
995 | :type :vorbis |
996 | :data (file->bytes (open |
997 | file |
998 | :element-type 'unsigned-byte))))) |
999 | </src> |
1000 | |
1001 | ** Dispatching |
1002 | |
1003 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/rerl/standard-dispatcher.lisp][src/rerl/standard-dispatcher.lisp]] |
1004 | |
1005 | <src lang="lisp"> |
1006 | (defgeneric matcher-match (matcher application context) |
1007 | (:documentation "Abstract method for subclasses to implement a |
1008 | matcher. This method would return multiple-values according to |
1009 | matcher internal nature. |
1010 | |
1011 | No methods defined on this function may rebind *context*, nor |
1012 | change CONTEXT's application. Only if the method matches the |
1013 | request, it is allowed to modify CONTEXT or APPLICATION, even in |
1014 | that case methods defined on this function must not modify |
1015 | CONTEXT's application nor rebind *context*.")) |
1016 | |
1017 | (defgeneric handler-handle (handler application context matcher-result) |
1018 | (:documentation "Abstract function for handler classes to |
1019 | implement in order to handle a request matched by relevant |
1020 | matcher. |
1021 | |
1022 | These methods may modify context as they wish since they'r |
1023 | matched, request will be closed after this method is run.")) |
1024 | |
1025 | (defgeneric dispatch (dispatcher application context) |
1026 | (:documentation "Entry point into a dispatcher. Must return T |
1027 | if the context has been handled or NIL if it hasn't. |
1028 | |
1029 | No methods defined on this function may rebind *context*, nor |
1030 | change CONTEXT's application. Only if the method returns T is it |
1031 | allowed to modify CONTEXT or APPLICATION, even in that case |
1032 | methods defined on this function must not modify CONTEXT's |
1033 | application nor rebind *context*.")) |
1034 | </src> |
1035 | |
1036 | <src lang="lisp"> |
1037 | (defclass my-matcher (abstract-matcher) ...) |
1038 | (defclass my-handler (abstract-handler) ...) |
1039 | (defclass my-dispatcher (abstract-dispatcher my-matcher my-handler) |
1040 | ...) |
1041 | </src> |
1042 | |
1043 | *** Simple Dispatcher |
1044 | |
1045 | <src lang="lisp"> |
1046 | (:documentation "This class of dispatchers avoids all of UCW's |
1047 | standard call/cc (and therefore frame/backtracking/component) |
1048 | mechanism. |
1049 | |
1050 | Unlike all other UCW dispatchers a simple-dispatcher must not use |
1051 | CALL, and must perform the rendering directly within the handler.") |
1052 | </src> |
1053 | |
1054 | ** Server |
1055 | |
1056 | [[http://www.uncommon-web.com/darcsweb/darcsweb.cgi?r=ucw_dev;a=headblob;f=/src/control.lisp][src/control.lisp]] |
1057 | |
1058 | <src lang="lisp"> |
1059 | (defun create-server (&key |
1060 | (backend `(,*ucw-backend-type* :host ,*ucw-backend-host* |
1061 | :port ,*ucw-backend-port*)) |
1062 | (applications *ucw-applications*) |
1063 | (start-p t) |
1064 | (server-class *ucw-server-class*) |
1065 | (log-root-directory (truename *ucw-log-root-directory*)) |
1066 | (log-level *ucw-log-level*)) |
1067 | "Creates and returns a UCW server according to SERVER-CLASS, HOST and |
1068 | PORT. Affects *DEFAULT-SERVER*. |
1069 | |
1070 | BACKEND is a list of (BACKEND-TYPE &rest INITARGS). BACKEND-TYPE |
1071 | may be :HTTPD, :MOD-LISP, :ASERVE, :ARANEIDA, an existing |
1072 | backend, an existing UCW server backend or :DEFAULT in which case |
1073 | it attempts to return a sane default from the UCW backends loaded |
1074 | and available, or any other value for which a valid MAKE-BACKEND |
1075 | method has been defined. INITARGS will be passed, unmodified, to |
1076 | MAKE-BACKEND. |
1077 | |
1078 | APPLICATIONS is a list of defined applications to be loaded into the |
1079 | server. |
1080 | |
1081 | Logs are generated in verbosity defined by LOG-LEVEL and directed to |
1082 | LOG-ROOT-DIRECTORY if defined." |
1083 | ... |
1084 | server) ; return server, naturally |
1085 | </src> |
1086 | |
1087 | ** Debugging |
1088 | |
1089 | *** Inspector |
1090 | |
1091 | [[pos:///home/clinton/src/ucw/darcs/ucw_dev/src/components/ucw-inspector.lisp#399][/home/clinton/src/ucw/darcs/ucw_dev/src/components/ucw-inspector.lisp]] |
1092 | |
1093 | <src lang="lisp"> |
1094 | (defaction call-inspector ((component component) datum) |
1095 | "Call an inspector for DATUM on the component COMPONENT." |
1096 | (call 'ucw-inspector :datum datum)) |
1097 | </src> |
1098 | |
1099 | * Tips |
1100 | |
1101 | ** Getting dojo to load |
1102 | |
1103 | I had some trouble getting dojo to work properly with UCW. The way |
1104 | that the =:www-roots= option for an application works is a bit |
1105 | confusing, and it is unforgiving if you mess the pathname up. A |
1106 | directory **must** have a =/= at the end, and the directory you are serving |
1107 | must also have the =/= (which is counterintuitive given the behavior of |
1108 | most unix things that don't want the =/= at the end of the name). |
1109 | |
1110 | <src lang="lisp"> |
1111 | :www-roots (list '("dojo/" . |
1112 | #P"/home/clinton/src/ucw/darcs/ucw_dev/wwwroot/dojo/")) |
1113 | </src> |
1114 | |
1115 | ** Specials Bound During Rendering |
1116 | |
1117 | The current request context is bound to =ucw:*context*=, and the current |
1118 | component is bound to =ucw:*current-component*= in the dynamic extent of |
1119 | =render=. |
1120 | |
1121 | ** Printing to the yaclml stream |
1122 | |
1123 | Occasionally it can be useful to do something like write a byte array |
1124 | as an ascii string to the client. Inside of =render= the variable |
1125 | =yaclml:*yaclml-stream*= is bound to the stream that you can write to if |
1126 | you wish to have content interspersed with yaclml tags. |