Ещё одна библиотека - cl-data-forms
При разработке веб-приложений есть такая неприятная вещь, как обработка форм. Вроде бы довольно тривиально, но без системного подхода превращается в какой-то мрак. Я давно облизывался на WTForms и вот теперь имею подходящее решение - cl-data-forms.
В принципе, cl-data-forms может быть использована не только для веб, никаких заточек под это там нет, но разработкой других приложений с пользовательским интерфейсом я пока на CL не занимаюсь.
Прежде, чем рассказывать про использование cl-data-forms, необходимо сказать несколько слов про data-sift. В принципе, я уже несколько раз упоминал эту библиотеку (и даже успел её использовать в RESTAS), но очень мало, плюс я несколько её переработал.
Там, где есть взаимодействие компьютера с человеком на основе текстовых форматов, возникает проблема преобразование внутренних представлений в текстовый, понятный для человека вид, и обратно. Мне очень понравилась библиотека cl-data-format-validation, призванная упростить эту проблему. Но с этой библиотекой есть одна проблема - она распространяется под лицензией GPL v3. Собственно, эта и подтолкнуло меня меня к созданию альтернативного решения под более мягкой лицензией.
data-sift определяет две обобщённые функции:
compile-parse-rule (rule &key &allow-other-keys)
compile-render-rule (rule &key &allow-other-keys)
compile-parse-rule принимает на вход правило, описывающее тип значения, и создаёт на его основе замыкание, которое может использоваться для валидации и, возможно, трансформации текстового значения, compile-render-rule делает совершенно обратное, т.е. возвращает функцию, которая может превратить значение в текст. Использовать это можно, например, так:
data-sift уже включает в себя поддержку нескольких форматов данных (правда, всё это пока довольно сыро), а в своём приложении можно определить дополнительные форматы, специфичные для данного приложения. Например, так можно определить формат date, соответствующий элементу <input type="date" /> из HTML5:
- (defmethod data-sift:compile-parse-rule ((rule (eql 'date)) &key)
- (alexandria:named-lambda date-parser (str)
- (handler-case
- (local-time:parse-rfc3339-timestring str :allow-missing-time-part t)
- (error ()
- (data-sift::vfail "Invalid date")))))
- (defmethod data-sift:compile-render-rule ((rule (eql 'date)) &key)
- (alexandria:named-lambda date-renderer (date)
- (local-time:format-timestring nil date :format local-time:+rfc3339-format/date-only+)))
Теперь можно вернуться к cl-data-forms. Сразу буду показывать код:
- (define-form-class user-info-form ()
- ((username
- :initarg |username|
- :initform nil
- :required "Username is required"
- :label "Username")
- :initarg |email|
- :initform nil
- :stype data-sift:email
- :required t
- :label "Email Address")))
- (define-form-class user-password-form ()
- ((password
- :initarg |password|
- :initform nil
- :required "Password is required"
- :stype (string :min-length 6 :message "Password must be at least 6 characters.")
- :label "Password"
- :itype "password")
- (confirm-password
- :initarg |confirmPassword|
- :initform nil
- :itype "password"
- :label "Confirm password")))
- (define-form-class registration-form (user-info-form user-password-form)
- ((birthday
- :initform nil
- :initarg |birthday|
- :stype date
- :label "Birthday"
- :itype "date")
- (keep-me-signed
- :initform nil
- :initarg |keepMeSigned|
- :label "Keep me signed-in on this computer."
- :itype "checkbox")))
Здесь определяется три формы:
user-info-form - предназначена для ввода имени пользователя и email
user-password-form - предназначенная для ввода пароля и его подтверждения
registration-form - включает в себя user-info-form и user-password-form, а также два дополнительных поля - день рождения и флаг "запомнить меня на этом компьютере"
Каждый макрос define-form-class создаёт новый класс. При описании слотов:
Можно использовать все те же самые параметры, что и при обычном defclass.
Параметр :initarg в описании слота является обязательным - он используется в последующем для получения данных формы. В коде выше вместо стандартных keyword-ов я использовал экранированные символы - так получается более красивый HTML (об этом ниже).
Параметр :requried используется для указания того, что поле является обязательным. Если указана строка, то она будет использоваться для создания сообщения об ошибке.
В параметре :stype можно указать формат для библиотеки data-sift и на его основе будет происходить проверка и преобразование данных.
Дополнительно можно указать любые другие параметры (в коде выше это :label и :itype) - они никак не обрабатываются, а просто сохраняются и могут быть использованы произвольным образом в зависимости от потребностей приложения.
Вот скриншот, полученный на основе registration-form (поскольку использован HTML5, то не во всех браузера поле для ввода даты будет именно таким, я использовал Chromium):

Объекты форм можно создавать:
С помощью стандартного make-instance
С помощью функции make-form, которая принимает имя класса формы и набор параметров в формате alist (как post-параметры в Hunchentoot). При этом, сопоставление параметров слотам производится на основе параметра :initarg, указанного при описании слота.
Проверка значения слота производится при каждом его изменении с помощью setf slot-value в том случае, если для задания нового значения используется строка. В случае ошибки валидации исключение не возбуждается (подавляется), а сообщение сохраняется во внутренней структуре формы, его можно получить с помощью field-error.
Важный момент, cl-data-forms не имеет никаких функций для генерации HTML. Вместо этого, она позволяет добавить в описание слота любые произвольные данные, которые могут быть получены вместе со значением и сообщением об ошибке при вызове функций: form-data-alist и form-data-plist (разница между ними только в формате). Например:
- EXAMPLE> (data-forms:form-data-alist (make-instance 'user-info-form '|username| "Andrey" '|email| "fake"))
- ((|username| (:VALUE . "Andrey")
- (:LABEL . "Username"))
- (|email| (:ERROR . "Doesn't look like a valid email.")
- (:VALUE . "fake")
- (:LABEL . "Email Address")))
На основе такого описания приложение может создавать HTML в своём собственном стиле. Для теста я использовал специальный шаблон для cl-closure-template.
Быстро узнать корректно ли заполнена форма можно с помощью функцию is-valid.
Полный код примера здесь , а то я и так уже слишком много написал. Для работы этого примера необходимы самые последние версии data-sift, restas, cl-closure-template и cl-data-forms.
P.S. Если посмотреть на описание формы, то видно, что оно полностью декларативное (за исключением пары нюансов). А значит, можно пробовать на основе такого описания генерировать JavaScript код для того, что проводить модные проверки валидности данных ещё на клиенте.