;; -*- 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) (ucw::dom-id :initform (unique-dom-id)))) ;;;; ** 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)))) (defgeneric form-fields-of (component)) (defmethod form-fields-of ((component standard-component)) "Return slot values of COMPONENT which are FORM-FIELDs" (remove-if-not #'ucw-forms::form-field-p (mapcar (arnesi:curry #'slot-value component) (remove-if-not #'(lambda (name) (slot-boundp component name)) (mapcar #'c2mop:slot-definition-name (c2mop:class-slots (class-of component))))))) (defmethod form-fields-of ((field form-field)) (list field)) (defun default-validp (form) ;; Mostly yanked from ucw update (C) notices (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)))) (let ((failed (apply #'nconc (mapcar #'do-validate-field (form-fields-of form))))) (values (null failed) failed)))) (defmethod validp ((form standard-component)) (default-validp form)) ;;;; ** 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))) (defmethod render ((field textarea-field)) (