Sunday, 13 April 2008

Weblocks: Presentations

When Slava set out to write Weblocks, he claims to have a goal to never write HTML again. The way he proposed to accomplish this was through the use of presentations. You can see an example of his UI-DSL. I don't claim to understand the UI-DSL but I do understand how presentations work and I like them!

I set out to add support for money for something I am working on. So first, I wanted to be able to validate/display a number. Simple enough. First thing I did was define a new type so I can change it later into something that handles currencies properly:

(deftype money () 

There are basically two things you need to be able to do (depending on context): parse and display. Obviously, presentations are responsible for the display while parsers are responsible for ... parsing!
(defun parse-money (string)
(read-from-string string))

(defun format-money-display (amount)
;; Note the extra "$"
(format nil "$~4$" amount))

(defun format-money-input (amount)
(format nil "~d" amount))

The reason there are two format functions is that you need to format the value for display or for editing. The latter is named format-money-input.

The protocol calls the generic functions weblocks:typespec->view-field-presentation and weblocks:typespec->form-view-field-parser depending on what the scaffold is rendering. The purpose of these functions is to take a typespec and create the corresponding presentation or parser.

You can have form scaffolds in addition to the regular scaffold. You would use the form scaffold when the method of display would be different. For example, for a boolean input you would use a checkbox for form input, whereas when simply rendering for display, you could use "true" or "false" or "yes" or "no".

In this case, I don't need anything this drastic, so I defined the functions as follows:
(defmethod weblocks:typespec->view-field-presentation (scaffold
(typespec (eql 'money)) args)
(values t (make-instance 'money-presentation)))

(defmethod weblocks:typespec->form-view-field-parser ((scaffold form-scaffold)
(typespec (eql 'money)) args)
(values t (make-instance 'money-parser)))

In this case, the money-presentation and money-parser types are just tags to make the generic function machinery work. The types are defined as follows:

(defclass money-presentation (weblocks:text-presentation weblocks:input-presentation)

(defclass money-parser (parser)
(:default-initargs :error-message "a money value (for example, 100.51 or 4.52)")
(:documentation "A parser designed to parse strings into

Using :default-initargs you can define an error message that will be seen when parsing fails (for whatever reason).

When Weblocks has your parser and presentation types, it then calls the following generic functions depending on the rendering context:

  • weblocks:print-view-field-value

  • weblocks:parse-view-field-value

  • weblocks:render-view-field-value

You won't believe it, but there is barely any HTML involved. I love it!

(defmethod weblocks:print-view-field-value (value
(presentation money-presentation)
field view widget obj &rest args)
(declare (ignore args))
(format-money-display value))

(defmethod weblocks:parse-view-field-value ((parser money-parser)
value obj
(view form-view) (field form-view-field)
&rest args)
(declare (ignore args))
(declare (optimize safety))
(let* ((presentp (text-input-present-p value))
(money-value (when presentp
(parse-money value))))
(values t presentp money-value))))

(defmethod weblocks:render-view-field-value (value (presentation money-presentation)
(field form-view-field)
(view form-view)
widget obj &rest args)
(declare (ignore args))
(render-text-input (view-field-slot-name field)
(format-money-input value)
:maxlength 50))

An example of usage would be:
(defclass transaction ()
((id :accessor transaction-id)
(date :initform (get-universal-time)
:accessor transaction-date
:initarg :date)
(amount :initform *default-money-value*
:accessor transaction-amount
:initarg :amount
:type money)))

And that is all you need to add your own reusable type to Weblocks. Not bad. Haven't added a date type yet, but will soon.


ndimiduk said...

In your definition of render-view-field-value, you use the method render-text-input which appears to be undefined. Is this a typo or am I missing something fundamental (an exercise left up to the reader, perhaps)? A quick grep of the weblocks src directory for that string returns no matches.

Thanks so much for the step-by-step example!

Sohail Somani said...

Ah, a careful reader :-)

That is a function I added to src/snippets/html-utils.lisp. I've pasted it here:

ndimiduk said...

Thank you kindly! In this case, it's less the astute reader and more the brain which requires a repl for the support of clear thought. ;)

Ben said...

Isn't using read-from-string in your parser a bit dangerous in a web application?
Or does the deftype 'number take care of this?
Seems you could just put arbitrary lisp code in your money field(albeit limited to 50 characters in your example...I think) otherwise.

Sohail Somani said...

Ben, you're definitely right about that. There are no inputs being sanitized. The only real way to write that function would be to write a money parser. Which depends on locale, etc, etc, etc...