[Initial commit clinton@unknownlamer.org**20081126041306 Imported form.lisp and portions of ucw-tags.lisp from a mixture of ucw_ajax and ucw_dev. Most of the code is from ucw_ajax, but none of this relies upon javascript and so some portions were easier to fetch from ucw_dev and update to work with ucw-core instead. ] adddir ./src addfile ./src/form.lisp hunk ./src/form.lisp 1 +;; -*- lisp -*- + +(in-package :ucw-forms) + +;;;; * UCW Web Form Library + +;;;; ** FORM-FIELDS + +;;;; Every input (or a set of inputs which correspond to one "value") +;;;; are represented by a form-field instance. + +(defclass form-field () + ((client-value :accessor client-value :initarg :client-value + :initform "" + :documentation "The string the client submitted along with this field.") + (validators :accessor validators :initform '() :initarg :validators + :documentation "List of validators which will be + applied to this field.") + (initially-validate :accessor initially-validate :initform t + :initarg :initially-validate + :documentation "When non-NIL the + validotars will be run as soon as the page + is rendered."))) + +(defmethod shared-initialize :after ((field form-field) + slot-names + &key (value nil value-p) &allow-other-keys) + (declare (ignore slot-names)) + (when value-p + (setf (value field) value))) + +(defprint-object (field form-field) + (format *standard-output* "~S" (value field))) + +(defgeneric form-field-p (object) + (:method ((object form-field)) t) + (:method ((object t)) nil)) + +(defgeneric value (form-field) + (:documentation "The lispish translated value that represents the form-field.")) + +(defgeneric (setf value) (new-value form-field) + (:documentation "Set the value of a form-field with translation to client.")) + +(defmethod value ((field form-field)) + (let ((client-value (client-value field))) + (if (string= client-value "") + nil + client-value))) + +(defmethod (setf value) (new-value (form-field form-field)) + (setf (client-value form-field) new-value)) + +(defclass html-input (form-field html-element-mixin) + ((name :accessor name :initarg :name :initform nil) + (accesskey :accessor accesskey :initarg :accesskey :initform nil) + (tooltip :accessor tooltip :initarg :tooltip :initform nil) + (tabindex :accessor tabindex :initarg :tabindex :initform nil))) + +;;;; ** Validators + +(defclass validator () + ((message :accessor message :initarg :message :initform nil))) + +(defgeneric validate (field validator) + (:documentation "Validate a form-field with a validator.")) + +;;;; *** validp Top-level function to determine the validity of a form field +(defgeneric validp (form-field) + (:documentation "Is a form-field valid?")) + +(defmethod validp ((field form-field)) + (loop + for validator in (validators field) + when (null (validate field validator)) + collect validator into failed-validators + finally (return (values (null failed-validators) + failed-validators)))) + +(defmethod validp ((component standard-component)) + "Loops over COMPONENT's slots, checks whethere all slots which + contain form-field objects are valid. + +As second value returns list of conses (invalid-field . failed-validator)." + ;;summary are we valid? + (flet ((do-validate-field (field) + "Run validators on FIELD, return list of (FIELD . VALIDATOR) conses." + (multiple-value-bind (validp failed) (validp field) + (declare (ignore validp)) + (mapcar #'(lambda (failed) (cons field failed)) + failed))) + + (form-fields-of (component) + "Return slot values of COMPONENT which are FORM-FIELDs" + (remove-if-not #'form-field-p + (mapcar (curry #'slot-value component) + (remove-if-not #'(lambda (name) (slot-boundp component name)) + (mapcar #'mopp:slot-definition-name + (mopp:class-slots (class-of component)))))))) + (let ((failed (apply #'nconc + (mapcar #'do-validate-field + (form-fields-of component))))) + (values (null failed) failed)))) + +;;;; ** Standard Form Validators + +;;;; *** not empty + +(defclass not-empty-validator (validator) + ()) + +(defmethod validate ((field form-field) (validator not-empty-validator)) + (and (client-value field) + (not (string= "" (client-value field))))) + +;;;; *** Value-validators + +;;;; These are validators that should only be run if there is actually +;;;; a value to test. + +(defclass value-validator (validator) + () + (:documentation "Validators that should only be applied if there is a value. +That is, they always succeed on nil.")) + +(defmethod validate :around ((field form-field) (validator value-validator)) + "Value validators should only be checked if they are not null or empty-strings." + (or (null (client-value field)) + (= 0 (length (string-trim '(#\Space #\Tab) (client-value field)))) + (call-next-method))) + +;;;; *** length + +(defclass length-validator (value-validator) + ((min-length :accessor min-length :initarg :min-length + :initform nil) + (max-length :accessor max-length :initarg :max-length + :initform nil))) + +(defmethod initialize-instance :after ((self length-validator) &key length min-length max-length) + (assert (or (null length) + (and (null min-length) + (null max-length)))) + (setf (min-length self) length) + (setf (max-length self) length)) + +(defmethod validate ((field form-field) (validator length-validator)) + (with-slots (min-length max-length) + validator + (let ((length (length (client-value field)))) + (cond + ((and min-length max-length) + (<= min-length length max-length)) + (min-length + (<= min-length length)) + (max-length + (<= length max-length)) + (t t))))) + +;;;; *** same (string=) value + +(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))) + +;;;; Regular expression validator. + +(defclass regex-validator (value-validator) + ((regex :accessor regex :initarg :regex :initform nil))) + +(defmethod validate ((field form-field) (validator regex-validator)) + (let ((val (string-trim '(#\Space #\Tab) (value field))) + (regex (regex validator))) + (or (null regex) + (cl-ppcre:scan regex val)))) + +;;;; *** simple e-mail address check. + +(defclass e-mail-address-validator (regex-validator) + () + (:default-initargs + :regex "^([a-zA-Z0-9_\\.\\-\\%])+\\@([a-zA-Z0-9][a-zA-Z0-9\\-]*\\.)+([a-zA-Z0-9]{2,4})\\.?$")) + +(defclass hostname-validator (regex-validator) + () + (:default-initargs + :regex "^([a-zA-Z0-9][a-zA-Z0-9\\-]*\\.)+([a-zA-Z0-9]{2,4})\\.?$")) + +(defclass phone-number-validator (regex-validator) + () + (:default-initargs + :regex "^((\\+\\d{1,3}(-| |\\.)?\\(?\\d\\)?(-| |\\.)?\\d{1,3})|(\\(?\\d{2,3}\\)?))(-| |\\.)?(\\d{3,4})(-| |\\.)?(\\d{4})(( x| ext)\\d{1,5}){0,1}$" )) + +;;;; ** Form Inputs + +;;;; *** Textarea inputs + +(defclass textarea-field (html-input) + ((rows :accessor rows :initarg :rows :initform nil) + (cols :accessor cols :initarg :cols :initform nil) + (wrap :accessor wrap :initarg :wrap :initform nil))) + +(defmethod render ((field textarea-field)) + (