Forms: Web Form Validation
(require forms) | package: forms-lib |
1 Introduction
This library lets you declaratively validate web form data. It differs from the formlets provided by Formlets: Functional Form Abstraction in two important ways:
validation and presentation are separate, and
you are given the ability to display and control validation errors.
1.1 Tutorial
1.1.1 Validation
forms are composed of other forms and formlets. A basic form might look like this:
(define simple-form (form (lambda (name) (and name (string-upcase name))) (list (cons 'name binding/text))))
This form accepts an optional text value named "name" and returns its upper-cased version. To validate some data against this form we can call form-validate:
> (form-validate simple-form (hash)) '(ok . #f)
> (form-validate simple-form (hash "name" (make-binding:form #"name" #"Bogdan"))) '(ok . "BOGDAN")
Formlets can be chained in order to generate more powerful validations. If we wanted the above form to require the "name" field, we’d combine binding/text with required using ensure:
(define simple-form (form (lambda (name) (string-upcase name)) (list (cons 'name (ensure binding/text (required))))))
If we validate the same data against simple-form now, our results differ slightly:
> (form-validate simple-form (hash)) '(err (name . "This field is required."))
> (form-validate simple-form (hash "name" (make-binding:form #"name" #"Bogdan"))) '(ok . "BOGDAN")
Notice how in the first example, an error was returned instead of #f and we no longer need to guard against false values in our lambda.
So far so good, but the syntax used to declare these forms can get unwieldy as soon as your forms grow larger than a couple fields. The library provides form*, which is a convenience macro designed to make writing large forms more manageable. In day-to-day use, you’d declare the above form like this:
(define simple-form (form* ([name (ensure binding/text (required))]) (string-upcase name)))
If you’re thinking "Hey, that looks like a let"! You’re on the right track.
1.1.2 Presentation
Let’s take a slightly more complicated form:
(define login-form (form* ([username (ensure binding/email (required) (shorter-than 150))] [password (ensure binding/text (required) (longer-than 8))]) (list username password)))
This form expects a valid e-mail address and a password longer than 8 characters and it returns a list containing the two values on success. To render this form to HTML, we can define a function that returns an x-expression:
(define (render-login-form) '(form ((action "") (method "POST")) (label "Username" (input ((type "email") (name "username")))) (label (input ((type "password") (name "password")))) (button ((type "submit")) "Login")))
This will do the trick, but it has two problems:
if there are any validation errors and we re-display the form to the user, the previously-submitted values won’t show up,
nor will any validation errors.
We can use widgets to fix both problems. First, we have to update render-login-form to take a widget-renderer/c as input:
(define (render-login-form render-widget) '(form ((action "") (method "POST")) (label "Username" (input ((type "email") (name "username")))) (label "Password" (input ((type "password") (name "password")))) (button ((type "submit")) "Login")))
Second, instead of rendering the input fields ourselves, we can tell render-widget to render the appropriate widgets for those fields:
(define (render-login-form render-widget) `(form ((action "") (method "POST")) (label "Username" ,(render-widget "username" (widget-email))) (label "Password" ,(render-widget "password" (widget-password))) (button ((type "submit")) "Login")))
Finally, we can also begin rendering errors:
(define (render-login-form render-widget) `(form ((action "") (method "POST")) (label "Username" ,(render-widget "username" (widget-email))) ,@(render-widget "username" (widget-errors)) (label "Password" ,(render-widget "password" (widget-password))) ,@(render-widget "password" (widget-errors)) (button ((type "submit")) "Login")))
To compose the validation and the presentation aspects, we can use form-run:
(define (make-request #:method [method #"GET"] #:url [url "http://example.com"] #:headers [headers null] #:bindings [bindings null]) (request method (string->url url) headers (delay bindings) #f "127.0.0.1" 8000 "127.0.0.1"))
> (form-run login-form (make-request)) '(pending #f #<procedure:...ib/private/form.rkt:115:2>)
form-run is smart enough to figure out whether or not the request should be validated based on the request method. Because we gave it a GET request above, it returned a 'pending result and a widget renderer. That same renderer can be passed to our render-login-form function:
> (match-define (list _ _ render-widget) (form-run login-form (make-request))) > (pretty-print (render-login-form render-widget))
'(form
((action "") (method "POST"))
(label "Username" (input ((type "email") (name "username"))))
(label "Password" (input ((type "password") (name "password"))))
(button ((type "submit")) "Login"))
If we pass it an empty POST request instead, the data will be validated and a 'failed result will be returned:
> (form-run login-form (make-request #:method #"POST")) '(failed ((username . "This field is required.") (password . "This field is required.")) #<procedure:...ib/private/form.rkt:115:2>)
Finally, if we pass it a valid POST request, we’ll get a 'passed result:
> (form-run login-form (make-request #:method #"POST" #:bindings (list (make-binding:form #"username" #"bogdan@defn.io") (make-binding:form #"password" #"hunter1234")))) '(passed ("bogdan@defn.io" "hunter1234") #<procedure:...ib/private/form.rkt:115:2>)
Putting it all together, we might write a request handler that looks like this:
(define (login req) (match (form-run login-form req) [(list 'passed (list username password) _) (login-user! username password) (redirect-to "/dashboard")] [(list _ _ render-widget) (response/xexpr (render-login-form render-widget))]))
1.1.3 Nested Validation
I left one thing out of the tutorial that you might be wondering about. Aside from plain values, forms can also return ok? or err? values. This makes it possible to do things like validate that two fields have the same value.
(define signup-form (form* ([username (ensure binding/email (required) (shorter-than 150))] [password (form* ([p1 (ensure binding/text (required) (longer-than 8))] [p2 (ensure binding/text (required) (longer-than 8))]) (cond [(string=? p1 p2) (ok p1)] [else (err "The passwords must match.")]))]) (list username password)))
This form will validate that the two password fields contain the same value and then return the first of them. When rendering the subform, you’d use widget-namespace to produce a widget renderer for the nested form’s fields.
1.1.4 Next Steps
If the tutorial left you wanting for more, take a look at the reference documentation below and also check out the examples folder in the source code repository.
2 Reference
2.1 Forms
struct
(struct form (constructor children) #:extra-constructor-name make-form) constructor : any/c children : (listof (cons/c symbol? (or (cons/c (or/c 'ok 'err) any/c) form?)))
Upon successful validation, the results of each of the children are passed in order to the constructor and an ok value is returned.
On failure, an err value is returned containing a list of errors for every child that failed validation.
syntax
(form* ([name formlet] ...+) e ...+)
procedure
(form-validate form bindings) → (cons/c (or/c 'ok 'err) any/c)
form : form? bindings : (hash/c string? any/c)
procedure
(form-run f r [ #:combine combine-proc #:defaults defaults #:submit-methods submit-methods])
→
(or/c (list/c 'passed any/c widget-renderer/c) (list/c 'failed any/c widget-renderer/c) (list/c 'pending #f widget-renderer/c)) f : form? r : request?
combine-proc : (-> any/c any/c any/c any/c) = (lambda (k v1 v2) v2) defaults : (hash/c string? binding?) = (hash)
submit-methods : (listof bytes?) = '(#"DELETE" #"PATCH" #"POST" #"PUT")
Changed in version 0.6 of package forms-lib: Added the #:combine argument.
2.2 Formlets
Formlets extract, validate and transform field values from forms.
value
binding/file :
(-> (or/c #f binding:file?) (or/c (cons/c 'ok (or/c #f binding:file?)) (cons/c 'err string?)))
value
binding/text :
(-> (or/c #f binding:form?) (or/c (cons/c 'ok (or/c #f string?)) (cons/c 'err string?)))
value
binding/boolean :
(-> (or/c #f binding:form?) (or/c (cons/c 'ok (or/c #f boolean?)) (cons/c 'err string?)))
value
binding/email :
(-> (or/c #f binding:form?) (or/c (cons/c 'ok (or/c #f string?)) (cons/c 'err string?)))
value
binding/number :
(-> (or/c #f binding:form?) (or/c (cons/c 'ok (or/c #f number?)) (cons/c 'err string?)))
value
binding/symbol :
(-> (or/c #f binding:form?) (or/c (cons/c 'ok (or/c #f symbol?)) (cons/c 'err string?)))
2.2.1 Primitives
These functions produce formlets either by combining other formlets or by "lifting" normal values into the formlet space.
procedure
(ensure f ...+) →
(-> any/c (or/c (cons/c 'ok any/c) (cons/c 'err string?)))
f :
(-> any/c (or/c (cons/c 'ok any/c) (cons/c 'err string?)))
2.2.2 Validators
These functions produce basic validator formlets.
procedure
(required [#:message message])
→
(-> (or/c #f any/c) (or/c (cons/c 'ok string?) (cons/c 'err string?))) message : string? = "This field is required."
procedure
(matches pattern [#:message message])
→
(-> (or/c string? #f) (or/c (cons/c 'ok string?) (cons/c 'err string?))) pattern : regexp?
message : string? = (format "This field must match the regular expression ~v." p)
procedure
(one-of pairs [#:message message])
→
(-> (or/c any/c #f) (or/c (cons/c 'ok any/c) (cons/c 'err string?))) pairs : (listof (cons/c any/c any/c))
message : string? = (format "This field must contain one of the following values: ~a" (string-join (map car pairs) ", "))
procedure
(shorter-than n [#:message message])
→
(-> (or/c string? #f) (or/c (cons/c 'ok string?) (cons/c 'err string?))) n : exact-positive-integer?
message : string? = (format "This field must contain ~a or fewer characters." (sub1 n))
procedure
(longer-than n [#:message message])
→
(-> (or/c string? #f) (or/c (cons/c 'ok string?) (cons/c 'err string?))) n : exact-positive-integer?
message : string? = (format "This field must contain ~a or more characters." (add1 n))
procedure
(to-integer [#:message message])
→
(-> (or/c number? #f) (or/c (cons/c 'ok real?) (cons/c 'err string?))) message : string? = "This field must contain an integer."
Added in version 0.6.1 of package forms-lib.
procedure
(to-real [#:message message]) →
(-> (or/c number? #f) (or/c (cons/c 'ok real?) (cons/c 'err string?))) message : string? = "This field must contain a real number."
procedure
(range/inclusive min max [#:message message])
→
(-> (or/c real? #f) (or/c (cons/c 'ok real?) (cons/c 'err string?))) min : real? max : real?
message : string? = (format "This field must contain a number that lies between ~a and ~a, inclusive." (~r min) (~r max))
2.3 Widgets
Widgets render fields into xexpr?s.
procedure
(widget-input #:type type [ #:omit-value? omit-value? #:attributes attributes]) → widget/c type : string? omit-value? : boolean? = #f attributes : attributes/c = null
procedure
(widget-errors #:class class) → widget/c
class : string?
> ((widget-errors) "example" #f null) '()
> ((widget-errors) "example" #f '((example . "this field is required") (another-field . "this field is required"))) '((ul ((class "errors")) (li "this field is required")))
procedure
(widget-checkbox [#:attributes attributes]) → widget/c
attributes : attributes/c = null
> ((widget-checkbox) "example" #f null) '(input ((type "checkbox") (name "example")))
> ((widget-checkbox) "example" (binding:form #"" #"value") null) '(input ((type "checkbox") (name "example") (value "value") (checked "checked")))
procedure
(widget-email [#:attributes attributes]) → widget/c
attributes : attributes/c = null
> ((widget-email) "example" #f null) '(input ((type "email") (name "example")))
> ((widget-email) "example" (binding:form #"" #"value@example.com") null) '(input ((type "email") (name "example") (value "value@example.com")))
procedure
(widget-file [#:attributes attributes]) → widget/c
attributes : attributes/c = null
> ((widget-file) "example" #f null) '(input ((type "file") (name "example")))
> ((widget-file) "example" (binding:file #"" #"filename" null #"content") null) '(input ((type "file") (name "example")))
procedure
(widget-hidden [#:attributes attributes]) → widget/c
attributes : attributes/c = null
> ((widget-hidden) "example" #f null) '(input ((type "hidden") (name "example")))
> ((widget-hidden) "example" (binding:form #"" #"value") null) '(input ((type "hidden") (name "example") (value "value")))
procedure
(widget-number [#:attributes attributes]) → widget/c
attributes : attributes/c = null
> ((widget-number) "example" #f null) '(input ((type "number") (name "example")))
> ((widget-number) "example" (binding:form #"" #"1") null) '(input ((type "number") (name "example") (value "1")))
procedure
(widget-password [#:attributes attributes]) → widget/c
attributes : attributes/c = null
> ((widget-password) "example" #f null) '(input ((type "password") (name "example")))
> ((widget-password) "example" (binding:form #"" #"value") null) '(input ((type "password") (name "example")))
procedure
(widget-select options [ #:attributes attributes]) → widget/c options : select-options/c attributes : attributes/c = null
> (define sel (widget-select '(("value-a" . "Label A") ("Countries" (("romania" . "Romania") ("usa" . "United States of America"))) ("Languages" (("english" . "English") ("racket" . "Racket")))))) > (pretty-print (sel "example" #f null))
'(select
((name "example"))
(option ((value "value-a")) "Label A")
(optgroup
((label "Countries"))
(option ((value "romania")) "Romania")
(option ((value "usa")) "United States of America"))
(optgroup
((label "Languages"))
(option ((value "english")) "English")
(option ((value "racket")) "Racket")))
> (pretty-print (sel "example" (binding:form #"" #"racket") null))
'(select
((name "example"))
(option ((value "value-a")) "Label A")
(optgroup
((label "Countries"))
(option ((value "romania")) "Romania")
(option ((value "usa")) "United States of America"))
(optgroup
((label "Languages"))
(option ((value "english")) "English")
(option ((value "racket") (selected "selected")) "Racket")))
procedure
(widget-radio-group options [ #:attributes attributes]) → widget/c options : radio-options/c attributes : attributes/c = null
> (define rg (widget-radio-group '(("value-a" . "Label A") ("value-b" . "Label B")))) > (pretty-print (rg "example" #f null))
'(div
(label (input ((type "radio") (name "example") (value "value-a"))) "Label A")
(label
(input ((type "radio") (name "example") (value "value-b")))
"Label B"))
> (pretty-print (rg "example" (binding:form #"" #"value-a") null))
'(div
(label
(input
((type "radio") (name "example") (value "value-a") (checked "checked")))
"Label A")
(label
(input ((type "radio") (name "example") (value "value-b")))
"Label B"))
procedure
(widget-text [#:attributes attributes]) → widget/c
attributes : attributes/c = null
> ((widget-text) "example" #f null) '(input ((type "text") (name "example")))
> ((widget-text) "example" (binding:form #"" #"value") null) '(input ((type "text") (name "example") (value "value")))
procedure
(widget-textarea [#:attributes attributes]) → widget/c
attributes : attributes/c = null
> ((widget-textarea) "example" #f null) '(textarea ((name "example")))
> ((widget-textarea) "example" (binding:form #"" #"value") null) '(textarea ((name "example")) "value")
procedure
(widget-namespace namespace widget-renderer) → widget/c namespace : string? widget-renderer : widget-renderer/c
2.3.1 Contracts
value
attributes/c : (listof (list/c symbol? string?))
value