6 Formlets: Functional Form Abstraction
(require web-server/formlets) | package: web-server-lib |
The Web Server provides a kind of Web form abstraction called a formlet.
Formlets originate in the work of the Links research group in their paper The Essence of Form Abstraction.
6.1 Basic Formlet Usage
Suppose we want to create an abstraction of entering a date in an HTML form. The following formlet captures this idea:
(define date-formlet (formlet (div "Month:" ,{input-int . => . month} "Day:" ,{input-int . => . day}) (list month day)))
The first part of the formlet syntax is the template of an X-expression that is the rendering of the formlet. It can contain elements like ,(=> formlet name) where formlet is a formlet expression and name is an identifier bound in the second part of the formlet syntax.
This formlet is displayed (with formlet-display) as the following X-expression forest (list):
(list '(div "Month:" (input ([name "input_0"])) "Day:" (input ([name "input_1"]))))
date-formlet not only captures the rendering of the form, but also the request processing logic. If we send it an HTTP request with bindings for "input_0" to "10" and "input_1" to "3", with formlet-process, then it returns:
(list 10 3)
which is the second part of the formlet syntax, where month has been replaced with the integer represented by the "input_0" and day has been replaced with the integer represented by the "input_1".
The real power of formlet is that they can be embedded within one another. For instance, suppose we want to combine two date forms to capture a travel itinerary. The following formlet does the job:
(define travel-formlet (formlet (div "Name:" ,{input-string . => . name} (div "Arrive:" ,{date-formlet . => . arrive} "Depart:" ,{date-formlet . => . depart}) (list name arrive depart))))
(Notice that date-formlet is embedded twice.) This is rendered as:
(list '(div "Name:" (input ([name "input_0"])) (div "Arrive:" (div "Month:" (input ([name "input_1"])) "Day:" (input ([name "input_2"]))) "Depart:" (div "Month:" (input ([name "input_3"])) "Day:" (input ([name "input_4"]))))))
Observe that formlet-display has automatically generated unique names for each input element. When we pass bindings for these names to formlet-process, the following list is returned:
(list "Jay" (list 10 3) (list 10 6))
In all these examples, we used the input-int and input-string formlets. Any value with the formlet contract can be used in these positions. For example, (to-string (required (text-input))) could be used as well. The rest of the manual gives the details of formlet usage, extension, and existing formlet combinators.
6.2 Static Syntactic Shorthand
(require web-server/formlets/syntax) | |
package: web-server-lib |
syntax
(formlet rendering-xexpr yields-expr)
(formlet (div ,@(for/list ([i (in-range 10)]) `(p ,((text-input) . => . name)))) name)
6.3 Dynamic Syntactic Shorthand
(require web-server/formlets/dyn-syntax) | |
package: web-server-lib |
syntax
(formlet* rendering-expr yields-expr)
(formlet* `(div ,@(for/list ([i (in-range 1 10)]) `(p ,(number->string i) ,((text-input) . =>* . name)))) name)
(formlet* `(div (p ,((text-input) . =>* . name))) name)
6.4 Functional Usage
(require web-server/formlets/lib) | package: web-server-lib |
value
Changed in version 1.3 of package web-server-lib: Fixed support for multiple return values.
value
Changed in version 1.3 of package web-server-lib: Fixed support for multiple return values.
procedure
(xml-forest r) → (formlet/c procedure?)
r : xexpr-forest/c
procedure
(xml r) → (formlet/c procedure?)
r : xexpr/c
procedure
(text r) → (formlet/c procedure?)
r : string?
6.5 Predefined Formlets
(require web-server/formlets/input) | |
package: web-server-lib |
procedure
(input #:type type [ #:value value #:size size #:max-length max-length #:read-only? read-only? #:attributes attrs]) → (formlet/c (or/c false/c binding?)) type : string? value : (or/c false/c bytes? string?) = #f size : (or/c false/c exact-nonnegative-integer?) = #f max-length : (or/c false/c exact-nonnegative-integer?) = #f read-only? : boolean? = #f attrs : (listof (list/c symbol? string?)) = empty
procedure
(text-input [ #:value value #:size size #:max-length max-length #:read-only? read-only? #:attributes attrs]) → (formlet/c (or/c false/c binding?)) value : (or/c false/c bytes? string?) = #f size : (or/c false/c exact-nonnegative-integer?) = #f max-length : (or/c false/c exact-nonnegative-integer?) = #f read-only? : boolean? = #f attrs : (listof (list/c symbol? string?)) = empty
procedure
(password-input [ #:value value #:size size #:max-length max-length #:read-only? read-only? #:attributes attrs]) → (formlet/c (or/c false/c binding?)) value : (or/c false/c bytes? string?) = #f size : (or/c false/c exact-nonnegative-integer?) = #f max-length : (or/c false/c exact-nonnegative-integer?) = #f read-only? : boolean? = #f attrs : (listof (list/c symbol? string?)) = empty
procedure
(textarea-input [ #:value value #:rows rows #:cols cols #:attributes attrs]) → (formlet/c (or/c false/c binding?)) value : (or/c false/c bytes? string?) = #f rows : (or/c false/c number?) = #f cols : (or/c false/c number?) = #f attrs : (listof (list/c symbol? string?)) = empty
procedure
(checkbox value checked? [#:attributes attrs])
→ (formlet/c (or/c false/c binding?)) value : (or/c bytes? string?) checked? : boolean? attrs : (listof (list/c symbol? string?)) = empty
procedure
(radio value checked? [#:attributes attrs])
→ (formlet/c (or/c false/c binding?)) value : (or/c bytes? string?) checked? : boolean? attrs : (listof (list/c symbol? string?)) = empty
procedure
(radio-group l [ #:attributes attrs #:checked? checked? #:display display #:wrap wrap]) → (formlet/c any/c) l : sequence?
attrs : (any/c . -> . (listof (list/c symbol? string?))) = (λ (x) empty) checked? : (any/c . -> . boolean?) = (λ (x) #f) display : (any/c . -> . xexpr/c) = (λ (x) x) wrap : (any/c any/c . -> . xexpr/c) = (λ (x y) (list x y))
procedure
(checkbox-group l [ #:attributes attrs #:checked? checked? #:display display #:wrap wrap]) → (formlet/c (listof any/c)) l : sequence?
attrs : (any/c . -> . (listof (list/c symbol? string?))) = (λ (x) empty) checked? : (any/c . -> . boolean?) = (λ (x) #f) display : (any/c . -> . xexpr/c) = (λ (x) x) wrap : (any/c any/c . -> . xexpr/c) = (λ (x y) (list x y))
procedure
(submit value [#:attributes attrs])
→ (formlet/c (or/c false/c binding?)) value : (or/c bytes? string?) attrs : (listof (list/c symbol? string?)) = empty
procedure
(reset value [#:attributes attrs])
→ (formlet/c (or/c false/c binding?)) value : (or/c bytes? string?) attrs : (listof (list/c symbol? string?)) = empty
procedure
(file-upload [#:attributes attrs])
→ (formlet/c (or/c false/c binding?)) attrs : (listof (list/c symbol? string?)) = empty
procedure
(hidden value [#:attributes attrs])
→ (formlet/c (or/c false/c binding?)) value : (or/c bytes? string?) attrs : (listof (list/c symbol? string?)) = empty
procedure
(img alt src [ #:height height #:longdesc ldesc #:usemap map #:width width #:attributes attrs]) → (formlet/c (or/c false/c binding?)) alt : (or/c bytes? string?) src : (or/c bytes? string?) height : (or/c false/c exact-nonnegative-integer?) = #f ldesc : (or/c false/c bytes? string?) = #f map : (or/c false/c bytes? string?) = #f width : (or/c false/c exact-nonnegative-integer?) = #f attrs : (listof (list/c symbol? string?)) = empty
procedure
(button type button-text [ #:disabled disabled #:value value #:attributes attrs]) → (formlet/c (or/c false/c binding?)) type : (or/c bytes? string?) button-text : (or/c bytes? string?) disabled : boolean? = #f value : (or/c false/c bytes? string?) = #f attrs : (listof (list/c symbol? string?)) = empty
procedure
(multiselect-input l [ #:attributes attrs #:multiple? multiple? #:selected? selected? #:display display]) → (formlet/c list?) l : sequence? attrs : (listof (list/c symbol? string?)) = empty multiple? : boolean? = #t selected? : (any/c . -> . boolean?) = (λ (x) #f) display : (any/c . -> . xexpr/c) = (λ (x) x)
procedure
(select-input l [ #:attributes attrs #:selected? selected? #:display display]) → (formlet/c any/c) l : sequence? attrs : (listof (list/c symbol? string?)) = empty selected? : (any/c . -> . boolean?) = (λ (x) #f) display : (any/c . -> . xexpr/c) = (λ (x) x)
procedure
(to-boolean f) → (formlet/c boolean?)
f : (formlet/c bytes?)
value
Changed in version 1.3 of package web-server-lib: Weakened result contract to allow any number.
value
6.6 Utilities
(require web-server/formlets/servlet) | |
package: web-server-lib |
procedure
(send/formlet f [ #:method method #:wrap wrapper]) →
any/c ... f : (formlet/c any/c ...) method : (or/c "GET" "POST" "get" "post") = "POST"
wrapper : (xexpr/c . -> . xexpr/c) =
(lambda (form-xexpr) `(html (head (title "Form Entry")) (body ,form-xexpr)))
procedure
(embed-formlet embed/url f) → xexpr/c
embed/url : ((request? . -> . any) . -> . string?) f : (formlet/c any/c ...)
6.7 Formlets and Stateless Servlets
(require web-server/formlets/stateless) | |
(require web-server/formlets/unsafe) | |
package: web-server-lib |
A few additional considerations apply when using formlets with stateless #lang web-server servlets.
First of all, continuations captured in your servlet cannot be serialized if they close over non-serializable data-structures. There are some generally-applicable ways to avoid having a data structure be part of the closure: for example, if you define all of your formlets as module-level variables, they will never be part of closures and will not need to be serialized. However, sometimes it can be useful to create formlets dynamically. To support this, all of the combinators documented above produce formlets that are serializable (as long as they contain only serializable values). This is not guaranteed to be true of third-party formlets.
One potential pitfall for formlets and serialization is pure. Note that (serialize (pure +)) will fail, because + is not serializable. To avoid this, you can write (pure (λ args (apply + args))) (in #lang web-server, where anonymous procedures are serializable, or using web-server/lang/serial-lambda).
Secondly, stateless #lang web-server servlets are based on different web interaction primitives than stateful servlets, so the version of send/formlet from web-server/formlets will not work. Instead, the library web-server/formlets/stateless provides the same API as web-server/formlets, but with a version of send/formlet for use in stateless servlets. (Using web-server/formlets/stateless also provides all of the bindings from web-server/formlets/lib, whereas web-server/formlets provides only some of them.) Alternatively, you can use the low-level formlet-process and formlet-display procedures directly.
Another issue concerns capturing continuations within the processing stage of a formlet. Recall that serializable continuations in #lang web-server can only be captured from within transformed contexts. The contract system is not transformed, so the contracts on this library prevent capturing continuations during the processing stage of formlets. In most cases, the best solution is simply to avoid using continuation-capturing operations during a formlet’s processing stage. Instead, have the processing stage return a value, and interact with the user based on that value in code outside of the formlet. Alternatively, you can use generally-applicable approaches for capturing continuations from untransformed contexts, such as web-server/lang/native. However, if neither of those approaches are satisfactory, the library web-server/formlets/unsafe provides the same API as web-server/formlets/stateless, but without enforcing contracts. As the name implies, using web-server/formlets/unsafe may produce inscrutable error messages and other unpleasant effects of programming without contracts: you have been warned.
Changed in version 1.3 of package web-server-lib: Added web-server/formlets/stateless and web-server/formlets/unsafe and changed combinators from web-server/formlets to produce serializable formlets.