Extension webactions
Well it seems that I had similiar ideas about how a web development tool might look as the implementor if kpax might have have also. However let me stress the following: The forthcoming code is totally undocumented and just used here internally. It combines a few ideas I had about web development, and it borrows from following things:
  • ucw
  • kpax
  • clim
    • Yeah you read that write it borrows from clim. On of it's major attractions to me is the Present/Accept Model. Some others have worked for me in this area and they ported this tool to the web surrounding. If there is some interest, then I might make it publicy available. Just let me know.

      Now here's my take on extending webactions into that direction

(in-package :qss.wa.shop) (defclass pane () ((view-name :accessor view-name :initarg :view-name) (view-value :accessor view-value :initarg :view-value :initform "") (model-slot :accessor model-slot :initarg :model-slot))) (defclass input-pane (pane) ((prompt :accessor prompt :initarg :prompt :initform "") (view-value-valid :accessor view-value-valid :initform t) (is-missing :accessor is-missing :initarg :is-missing :initform nil))) (defclass checkbox-pane (input-pane) ((default :accessor default :initarg :default))) (defclass text-input-pane (input-pane) ((size :accessor size :initarg :size :initform "20") (maxlength :accessor maxlength :initarg :maxlength :initform "20"))) (defclass text-area-input-pane (input-pane) ((rows :accessor rows :initarg :rows :initform "10") (cols :accessor cols :initarg :cols :initform "50"))) (defclass password-input-pane (text-input-pane) ()) (defclass integer-input-pane (text-input-pane) ()) (defclass option-input-pane (input-pane) ((default :accessor default :initarg :default :initform nil) (options :accessor options :initarg :options :type list))) (defclass view () ()) (defclass html-view (view) ()) (defclass html-table-view (html-view) ()) (defparameter +html-view+ (make-instance 'html-view)) (defparameter +html-table-view+ (make-instance 'html-table-view)) (defclass form () ((panes :accessor panes :initarg :panes :type list))) (defclass prompt-form (form) ()) (defgeneric form-validator (form req) (:documentation "A collection to validata a whole form, will probably call into view-validator which is used for validating one visual element")) (defgeneric view-validator (input-pane)) (defgeneric prompt-for (thing view stream) (:method ((pane text-input-pane) (view html-view) stream) (if (is-missing pane) (html-stream stream ((:span :class "missing") ((:input :type "text" :name (view-name pane) :size (size pane) :maxlength (maxlength pane) :value "")))) (html-stream stream ((:input :type "text" :name (view-name pane) :size (size pane) :maxlength (maxlength pane) :value (view-value pane) ))))) (:method ((pane text-area-input-pane) (view html-view) stream) (if (is-missing pane) (html-stream stream ((:span :class "missing") ((:textarea :name (view-name pane) :cols (cols pane) :rows (rows pane)) ""))) (html-stream stream ((:textarea :name (view-name pane) :cols (cols pane) :rows (rows pane)) (:princ-safe (view-value pane)))))) ;;; ATTENTION redundant code could be factored out (:method ((pane password-input-pane) (view html-view) stream) (if (is-missing pane) (html-stream stream ((:span :class "missing") ((:input :type "password" :name (view-name pane) :size (size pane) :maxlength (maxlength pane) :value "")))) (html-stream stream ((:input :type "password" :name (view-name pane) :size (size pane) :maxlength (maxlength pane) :value ""))))) (:method ((pane option-input-pane) (view html-view) stream) (html-stream stream ((:select :name (view-name pane)) (dolist (option (options pane)) (html-stream stream (if (string= option (default pane)) (progn (inspect option) (html-stream stream ((:option :selected "selected") (write-string option stream)))) (html-stream stream (:option (write-string option stream)))))))))) ;; I feel the prompt and present stuff can be combined ;; just for the time beeing I separate them (defgeneric present (thing view stream) (:method ((thing text-input-pane) (view html-table-view) stream) (write-string (view-value thing) stream)))

Agreed, some of you can write this much more elegant. But for my use it's "good" enough and "extensible" enough.

As said it's not documented at all, because it was/is my first take on it.

Just so much view-name is the name I like to give the visual control (in some web-form) view-value is the containing value model-slot is a link to a slot of a Database object.

The kpax developers have similiar but much more elaborated scheme. AFAIKS I would go in the same direction.

So here's what the kpax developers have written:

(defclass web-form-field-definition () ((parent :accessor get-parent :initarg :parent) (name :accessor get-name :initarg :name) (label :accessor get-label :initform nil) (type :accessor get-type :initarg :type) (formatter :accessor get-formatter :initform #'princ-to-string) (parser :accessor get-parser :initform #'identity) (validator :accessor get-validator :initform (constantly t)) (options :accessor get-options :initform nil) (comments :accessor get-comments :initform nil)))

As you see they have though a little bit further. The have a formatter function, a parser function and a validator function tight to the visual element. But what they do not have is a link to some external objects (for persistence e.g).

My idea is that updates may look like this:

(defmethod update-model-from-view ((view input-pane) (model t1-class)) (setf (slot-value model (slot-value view 'model-slot)) (view-value view)))

The kpax Developers have written:

(defun copy-slots-form->object (form object slots) "Copy the named slots from a form to an object" (dolist (slot slots) (cond ((symbolp slot) (setf (slot-value object slot) (field-value form slot))) ((consp slot) (setf (slot-value object (cdr slot)) (field-value form (car slot)))) (t (error "unknown slot spec ~s" slot)))))

But as you can see this approaches are nearly identical. And this is an interesting coincidence. I did not know how that the kpax people did it that way.

Now let's see some small example:

How I do a form

;;;; Input panes for addresses ;; It's obvious that this code can be generated, just for now ;; write it down by hand (defclass address-street (text-input-pane) ((prompt :initform "Your address:") (view-name :initform "adr-street") (model-slot :initform 'qss.db:adr-street))) (defclass address-city (text-input-pane) ((prompt :initform "Your city:") (view-name :initform "adr-city") (model-slot :initform 'qss.db:adr-city))) (defclass address-zip (text-input-pane) ((prompt :initform "Your zip-code:") (view-name :initform "adr-zip") (model-slot :initform 'qss.db:adr-zip))) (defclass address-country (text-input-pane) ((prompt :initform "Your country:") (view-name :initform "adr-country") (model-slot :initform 'qss.db:adr-country))) ;;; the address input form (defclass address-input-form (form) ((address-street :accessor address-street :initform (make-instance 'address-street)) (address-city :accessor address-city :initform (make-instance 'address-city)) (address-zip :accessor address-zip :initform (make-instance 'address-zip)) (address-country :accessor address-country :initform (make-instance 'address-country)))) (defmethod initialize-instance :after ((form address-input-form) &rest initargs) "This step is done to have a 'flat' view into the form, this probably won't work anytime, but if it does it eases the validation and output massivly, instead of x calls one can use a loop." (declare (ignore initargs)) (setf (panes form) (list (address-street form) (address-city form) (address-zip form) (address-country form))))

I do not have the exact counterpart, but some excerpt from it:

The kpax way

(defwebform demo-web-form ((:group personal-info :label "Personal Info" :members ((fullname :text :label "Fullname") (username :text :label "Username" :options (:size 10) :validator (all required (limited-string 8 4) contains-no-spaces)) (password :password :label "Password" :options (:size 10) :validator (or optional (limited-string 32))) (password2 :password :label "Password" :options (:size 10) :comment "[Confirmation]" :validator (or optional (limited-string 32))) (gender :choice :options (:values *genders* :style :buttons) :parser parse-keyword :formatter string-capitalize :validator (or optional (list-element *genders*))) (age :text :label "Age" :options (:size 4) :parser s-utils:parse-integer-safely :validator (integer-range 0 150))))

Now it gets interesting. We can compare the design decisions: The kpax Developers have tighten the validators to the slots of a class, I let them stay alone as "generic" functions", the ucw folks have bind it to the slots of the class also. The charactristics are therefor:

  • the ucw and kpax people have to think about the validators while defining this form.
  • In my solution this is deferred to later.
Of course that is not fully true, because they can give the slot a new value later also.

But their example has one advantage: They can see on one sight what they can expect for the slots of their view class. This is not true for my solution. You have to browse

  • the class
  • the generic functions (for parsing, presenting, formatting, validation)

Of course my solution needs less memory. But this hardly could be a problem nowadays. So I'd argue the decisions from the ucw and kpax are the better ones.

What do you think?


web development webactions kpax ucw