Html templating: TAL <- back | toc | forward -> Debugging Tools
Advanced form handling
Ucw has some extra classes and methods defined that operate on forms so as to make making them easier and checking what's in them safer. In this model every input field becomes a class which handles for example strings or integers and to which you can assign validators which check the inbound fields whatever you want to check. Also pre-defined render methods spit out html to let everything look nice and they spit out javascript to do validation on the client side (which is complimentary, not necessary). Source for the chapter are here to be found.
Just to see how a bare boned and all washed up form might look and to not having to explain everything in one go, here is a form with a field which checks its content on being not empty and on being between four and eighteen characters.
(defcomponent basic-form (simple-form) ((basic-field :accessor basic-field-of :initform (make-instance 'string-field :validators (list (make-instance 'not-empty-validator) (make-instance 'length-validator :min-length 4 :max-length 18))))) (:render () (render (basic-field-of basic-form))))
If you use the sheet.css file supplied with the intro sources, you will see a red border around the field since it's empty. If you type in at least four characters it becomes green. Foxy ey?
The simple-form is a <:ucw form which refreshes itself on submit. The string field class we see here is, you could say, the standard field class to use. It's got a number of slots it inherits and a number of methods defined on it so as to let the machinery sail smoothly. The most important method for a user is the value method, which also defined on all other fields. It retrieves the value you want to get to, which in this case (and usually) resides in the client-value slot. We'll list all slots and classes in a moment.
The validators slot we see here takes a list of validator classes. When you add one (pre-defined or your own) it renders some javascript on the page which checks the form field on the client side, and it provides methods to check your field on the server side.
On the client side javascript code checks the field on input and matches it against the validators. If the input is correct, the fields class is ucw-form-field-valid. If it is incorrect, the class will switch to ucw-form-field-invalid. If it is correct again, the class will switch to ucw-form-field-valid. And so on.
On the server-side you can use the method validp on the component that harbors the fields to check if all of them pass their validator tests. It returns t if they do and nil if they don't. You can also use validp on a single field. Then it also returns t and nil, but as a second value it returns the validators that failed. For example in the example code for this chapter we use a function print-unless-valid to print a message when a field is valid:
(defun print-unless-valid (field name) (multiple-value-bind (validp failed-validators) (validp field) (unless validp (<:br) (<:ah name " has troubles with " failed-validators))))
This should be enough info to give you a feel for it.
The fields
The father of all input fields is the form-field which itself inherits some slots from widget-component; eg: css-class, css-style and css-id. Supply fit values for your own use if appropriate. Form-fields add a validators slot and a initially-validate slot. The validators slot should contain a list of validators which will be applied to the field and the initially-validate slot (which is set to t by default) will execute javascript validators on page load.
On top of form-field, generic-html-input is built. It adds the classes dom-id, client-value and name. Dom-id is the dom-id, which by default is given a value through its initform. The name is the name of the field, as in the html attribute (also set by ucw) and the client value is the value the user supplies.
Then we have the end-user sort of fields that we actually work with:
- textarea-field - inherits from generic-html-input. Adds the slots rows and cols, which specify the.. sigh.. number of rows and columns of the textarea. Renders a <ucw:textarea.
- string-field - inherits from generic-html-input. Adds the slot input-size which designates the size of the field. Renders a <ucw:text
- in-field-string-field - inherits from string-field. adds the slot in-field-label. Basically the same as a string-field but the in-field-label text string is used as an initial value for the field, which is taken away as soon as the user edits the field.
- password-field - inherits from string-field. Adds no slots. Renders a field in <ucw:password.
- number-field - inherits from string-field. Adds no slots. Expects the string to represent a base 10 number. Uses render method from string-field. Uses is-a-number-validator.
- integer-field - inherits from number-field. Adds no slots. Expects an integer. Uses render method from string field. Uses is-an-integer-validator.
- date-field - inherits from form-field. Adds the slots year, month, day which are integer-fields. Isn't fit for rendering.
- dmy-date-field - inherits from date-field. Adds no slots. Renders its year, month and day integer-fields in the order d/m/y.
- mdy-date-field - inherits from date-field. Adds no slots. Renders its year, month and day integer-fields in the order m/d/y.
- checkbox-field - inherits from generic-html-input. Adds no slots. Renders a <ucw:input with :id, :checked, :name, :reader, :writer and :type filled by the class slots.
- file-upload-field - inherits from generic-html-input. Adds no slots. Renders a <ucw:input with :id :name and :accessor form the class and sets :type to file.
- submit-button - inherits from generic-html-input. Adds the slot label. Renders a <:submit with :id and :value taken from the class.
- select-field - inherits from generic-html-input. Adds the slots data-set, eg a list of values the select chooses from, test-fn which defaults to string=, and data-map which is an alist for internal mapping.
- hash-table-select-field - inherits from select-field. Adds no slots. Data-set is a hash table. The key of each entry is used as the name of an option. The value is the value which you retrieve with the value function.
- alist-select-field - inherits from select-field. Adds no slots. Data-set is an alist. The key of each entry is used as the name of an option. The value is the value which you retrieve with the value function.
- plist-select-field - inherits from select-field. Adds no slots. Data-set is a plist. The key of each entry is used as the name of an option. The value is the value which you retrieve with the value function.
validators
Each validator class should have two methods defined on it:
- a validate method, which takes a field and should return t for a successful validation and nil for an unsuccessful one.
- a javascript-check method which should contain parenscript code which should compile to javascript code that should equate to true or null, depending on the outcome of the validation.
As a reference, here you have the implementation of the string=-validator which checks if one field is string= to another:
(defclass string=-validator (validator) ((other-field :accessor other-field :initarg :other-field)) (:documentation "Ensures that a field is string= to another one."))
(defmethod validate ((field form-field) (validator string=-validator)) (let ((value (value field)) (other-value (value (other-field validator)))) (string= value other-value)))
(defmethod javascript-check ((field form-field) (validator string=-validator)) `(= (slot-value (document.get-element-by-id ,(dom-id field)) 'value) (slot-value (document.get-element-by-id ,(dom-id (other-field validator))) 'value)))
Here's an overview of the validators:
- validator - Validator base class. Holds message slot, to put in a message you want to print on failed validation.
- not-empty-validator - checks if the field is not empty. Adds no slots. Inherits from validator.
- value-validator - Base validator for validators that should only be applied if there is a value. That is, it always succeed on nil. Adds no slots. Inherits from validator.
- length-validator - Checks that amount of characters in field is between min-length and max-length. Adds the slots min-length and max-length. Inherits from value-validator.
- string=-validator - Checks if the field is string= to another field. Adds an other-field slot to hold a reference to the other field. Inherits from validator.
- regex-validator - Compares the field to a regular expression. Adds a regex slot. Inherits from value-validator.
- email-adress-validator - Compares the field against an email-address regex stored in its regex slot. Adds no slot. Inherits from regex-validator.
- phone-number-validator - Compares the field against a phone number regex stored in its regex slot. Adds no slot. Inherits from regex-validator.
- is-a-number-validator - Checks if a string represents a number. Adds no slots. Inherits from value-validator.
- number-range-validator - Checks if a number is between min-value and max-value. Adds the slots min-value and max-value. Inherits from is-a-number-validator.
- is-an-integer-validator - Checks if a string represents an integer. Adds no slots. Inherits from is-a-number-validator.
example code
To show off the items discussed in this chapter, here's some code which lists enough. It's just a lot of code which all pretty much does the same. Make a call from somewhere in your application to the forms component (in the tabbed pane and through ucw-intro/forms.ucw in the supplied source) and make a file with this content:
(in-package :ucw-intro)
(defun print-unless-valid (field name) (multiple-value-bind (validp failed-validators) (validp field) (unless validp (<:br) (<:ah name " has troubles with " failed-validators))))
(defmacro render-slot (field name) `(<:p (render ,field) " " ,name (print-unless-valid ,field ,name)))
(defcomponent forms (simple-window-component) ((form1 :accessor form1-of :initform (make-instance 'basic-form)) (form2 :accessor form2-of :initform (make-instance 'mucho-forms)) (select :accessor select-of :initform 1) (select2 :accessor select2-of :initform 2) (select3 :accessor select3-of :initform 3)) (:default-initargs :stylesheet "sheet.css" :title "forms" :javascript '((:src "dojo.js") (:js (dojo.require "dojo.event.*")))) (:render () (<:p "a string field" (render (form1-of forms))) (<:p "the rest" (<:br) (render (form2-of forms)))))
(defcomponent basic-form (simple-form) ((basic-field :accessor basic-field-of :initform (make-instance 'string-field :input-size 20 :validators (list (make-instance 'not-empty-validator) (make-instance 'length-validator :min-length 4 :max-length 18))))) (:render () (render-slot (basic-field-of basic-form) "a string-field")))
(defcomponent mucho-forms (simple-form) ((textarea-slot :accessor textarea-slot-of :initform (make-instance 'textarea-field)) (in-field-string-slot :accessor in-field-string-slot-of :initform (make-instance 'in-field-string-field :in-field-label "click me and i'm gone")) (password-slot :accessor password-slot-of :initform (make-instance 'password-field)) (email-slot :accessor email-slot-of :initform (make-instance 'string-field :validators (list (make-instance 'e-mail-address-validator)))) (phone-number-slot :accessor phone-number-slot-of :initform (make-instance 'string-field :validators (list (make-instance 'phone-number-validator)))) (number-slot :accessor number-slot-of :initform (make-instance 'number-field)) (integer-slot :accessor integer-slot-of :initform (make-instance 'integer-field)) (dmy-date-slot :accessor dmy-date-slot-of :initform (make-instance 'dmy-date-field)) (mdy-date-slot :accessor mdy-date-slot-of :initform (make-instance 'mdy-date-field)) (select-slot :accessor select-slot-of :initform (make-instance 'alist-select-field :data-set '(("appel" . "banaan") ("ei" . "snaak") ("gabber" . "snol")))) (checkbox-slot :accessor checkbox-slot-of :initform (make-instance 'checkbox-field)) (submit-slot :accessor submit-slot-of :initform (make-instance 'submit-button :label "push me"))) (:render () (render-slot (textarea-slot-of mucho-forms) "the textarea slot") (render-slot (password-slot-of mucho-forms) "the password slot") (render-slot (in-field-string-slot-of mucho-forms) "the in-field string slot") (render-slot (email-slot-of mucho-forms) "the email slot") (render-slot (phone-number-slot-of mucho-forms) "the phone number slot") (render-slot (number-slot-of mucho-forms) "the number slot") (render-slot (integer-slot-of mucho-forms) "the integer slot") (render-slot (dmy-date-slot-of mucho-forms) "the dmy date slot") (render-slot (mdy-date-slot-of mucho-forms) "the mdy date slot") (render-slot (select-slot-of mucho-forms) "the select slot ") (<:p "value of alist-select: " (<:ah (value (select-slot-of mucho-forms)))) (render-slot (checkbox-slot-of mucho-forms) "checkbox slot") (render-slot (submit-slot-of mucho-forms) "the submit slot")))
(defmethod shared-initialize :after ((form mucho-forms) slot-names &rest initargs) (declare (ignore slot-names initargs)) (push (make-instance 'string=-validator :other-field (textarea-slot-of form)) (validators (password-slot-of form))))
Html templating: TAL <- back | toc | forward -> Debugging Tools