← Blog of oldlisper

Ещё одна библиотека - cl-data-forms

· 20.01.2013 00:00
· original author: archimag

Ещё одна библиотека - 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 делает совершенно обратное, т.е. возвращает функцию, которая может превратить значение в текст. Использовать это можно, например, так:

  1. (funcall (data-sift:compile-parse-rule 'integer :min-value 0 :max-value 100) "56")

data-sift уже включает в себя поддержку нескольких форматов данных (правда, всё это пока довольно сыро), а в своём приложении можно определить дополнительные форматы, специфичные для данного приложения. Например, так можно определить формат date, соответствующий элементу <input type="date" /> из HTML5:

  1. (defmethod data-sift:compile-parse-rule ((rule (eql 'date)) &key)
  2.   (alexandria:named-lambda date-parser (str)
  3.     (handler-case
  4.         (local-time:parse-rfc3339-timestring str :allow-missing-time-part t)
  5.       (error ()
  6.         (data-sift::vfail "Invalid date")))))
  7. (defmethod data-sift:compile-render-rule ((rule (eql 'date)) &key)
  8.   (alexandria:named-lambda date-renderer (date)
  9.     (local-time:format-timestring nil date :format local-time:+rfc3339-format/date-only+)))

Теперь можно вернуться к cl-data-forms. Сразу буду показывать код:

  1. (define-form-class user-info-form ()
  2.   ((username
  3.     :initarg |username|
  4.     :initform nil
  5.     :required "Username is required"
  6.     :label "Username")
  7.    (email
  8.     :initarg |email|
  9.     :initform nil
  10.     :stype data-sift:email
  11.     :required t
  12.     :label "Email Address")))
  13. (define-form-class user-password-form ()
  14.   ((password
  15.     :initarg |password|
  16.     :initform nil
  17.     :required "Password is required"
  18.     :stype (string :min-length 6 :message "Password must be at least 6 characters.")
  19.     :label "Password"
  20.     :itype "password")
  21.    (confirm-password
  22.     :initarg |confirmPassword|
  23.     :initform nil
  24.     :itype "password"
  25.     :label "Confirm password")))
  26. (define-form-class registration-form (user-info-form user-password-form)
  27.   ((birthday
  28.     :initform nil
  29.     :initarg |birthday|
  30.     :stype date
  31.     :label "Birthday"
  32.     :itype "date")
  33.    (keep-me-signed
  34.     :initform nil
  35.     :initarg |keepMeSigned|
  36.     :label "Keep me signed-in on this computer."
  37.     :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):

Скриншот HTML формы

Объекты форм можно создавать:

  • С помощью стандартного make-instance

  • С помощью функции make-form, которая принимает имя класса формы и набор параметров в формате alist (как post-параметры в Hunchentoot). При этом, сопоставление параметров слотам производится на основе параметра :initarg, указанного при описании слота.

Проверка значения слота производится при каждом его изменении с помощью setf slot-value в том случае, если для задания нового значения используется строка. В случае ошибки валидации исключение не возбуждается (подавляется), а сообщение сохраняется во внутренней структуре формы, его можно получить с помощью field-error.

Важный момент, cl-data-forms не имеет никаких функций для генерации HTML. Вместо этого, она позволяет добавить в описание слота любые произвольные данные, которые могут быть получены вместе со значением и сообщением об ошибке при вызове функций: form-data-alist и form-data-plist (разница между ними только в формате). Например:

  1. EXAMPLE> (data-forms:form-data-alist (make-instance 'user-info-form '|username| "Andrey" '|email| "fake"))
  2. ((|username| (:VALUE . "Andrey")
  3.              (:LABEL . "Username"))
  4.  (|email| (:ERROR . "Doesn't look like a valid email.")
  5.           (:VALUE . "fake")
  6.           (:LABEL . "Email Address")))

На основе такого описания приложение может создавать HTML в своём собственном стиле. Для теста я использовал специальный шаблон для cl-closure-template.

Быстро узнать корректно ли заполнена форма можно с помощью функцию is-valid.

Полный код примера здесь , а то я и так уже слишком много написал. Для работы этого примера необходимы самые последние версии data-sift, restas, cl-closure-template и cl-data-forms.

P.S. Если посмотреть на описание формы, то видно, что оно полностью декларативное (за исключением пары нюансов). А значит, можно пробовать на основе такого описания генерировать JavaScript код для того, что проводить модные проверки валидности данных ещё на клиенте.