531 lines
20 KiB
Racket
531 lines
20 KiB
Racket
#lang scribble/doc
|
|
@(require "web-server.rkt")
|
|
@(require (for-label web-server/servlet
|
|
racket/list
|
|
xml))
|
|
|
|
@(define xexpr @tech[#:doc '(lib "xml/xml.scrbl")]{X-expression})
|
|
|
|
@title[#:tag "formlets"]{Formlets: Functional Form Abstraction}
|
|
|
|
@defmodule[web-server/formlets]
|
|
|
|
The @web-server provides a kind of Web form abstraction called a @tech{formlet}.
|
|
|
|
@margin-note{@tech{Formlet}s originate in the work of the @link["http://groups.inf.ed.ac.uk/links/"]{Links} research group in
|
|
their paper @link["http://groups.inf.ed.ac.uk/links/formlets/"]{The Essence of Form Abstraction}.}
|
|
|
|
@section{Basic Formlet Usage}
|
|
|
|
Suppose we want to create an abstraction of entering a date in an HTML form. The following
|
|
@deftech{formlet} captures this idea:
|
|
|
|
@racketblock[
|
|
(define date-formlet
|
|
(formlet
|
|
(div "Month:" ,{input-int . => . month}
|
|
"Day:" ,{input-int . => . day})
|
|
(list month day)))
|
|
]
|
|
|
|
The first part of the @racket[formlet] syntax is the template of an @xexpr that is the rendering
|
|
of the formlet. It can contain elements like @racket[,(_formlet . => . _name)] where @racket[_formlet]
|
|
is a formlet expression and @racket[_name] is an identifier bound in the second part of the @racket[formlet]
|
|
syntax.
|
|
|
|
This formlet is displayed (with @racket[formlet-display]) as the following @xexpr forest (list):
|
|
|
|
@racketblock[
|
|
(list
|
|
'(div "Month:" (input ([name "input_0"]))
|
|
"Day:" (input ([name "input_1"]))))
|
|
]
|
|
|
|
@racket[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 @racket["input_0"] to @racket["10"] and
|
|
@racket["input_1"] to @racket["3"], with @racket[formlet-process], then it returns:
|
|
|
|
@racketblock[
|
|
(list 10 3)
|
|
]
|
|
|
|
which is the second part of the @racket[formlet] syntax, where @racket[month] has been replaced with the
|
|
integer represented by the @racket["input_0"] and @racket[day] has been replaced with the
|
|
integer represented by the @racket["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:
|
|
|
|
@racketblock[
|
|
(define travel-formlet
|
|
(formlet
|
|
(div
|
|
"Name:" ,{input-string . => . name}
|
|
(div
|
|
"Arrive:" ,{date-formlet . => . arrive}
|
|
"Depart:" ,{date-formlet . => . depart})
|
|
(list name arrive depart))))
|
|
]
|
|
|
|
(Notice that @racket[date-formlet] is embedded twice.) This is rendered as:
|
|
|
|
@racketblock[
|
|
(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 @racket[formlet-display] has automatically generated unique names for each input element. When we pass
|
|
bindings for these names to @racket[formlet-process], the following list is returned:
|
|
|
|
@racketblock[
|
|
(list "Jay"
|
|
(list 10 3)
|
|
(list 10 6))
|
|
]
|
|
|
|
In all these examples, we used the @racket[input-int] and
|
|
@racket[input-string] formlets. Any value with the @tech{formlet}
|
|
contract can be used in these positions. For example,
|
|
@racket[(to-string (required (text-input)))] could be used as
|
|
well. The rest of the manual gives the details of @tech{formlet}
|
|
usage, extension, and existing formlet combinators.
|
|
|
|
@section{Static Syntactic Shorthand}
|
|
|
|
@(require (for-label web-server/formlets/syntax))
|
|
@defmodule[web-server/formlets/syntax]{
|
|
|
|
Most users will want to use the syntactic shorthand for creating @tech{formlet}s.
|
|
|
|
@defform[(formlet rendering-xexpr yields-expr)]{
|
|
Constructs a @tech{formlet} with the specified @racket[rendering-xexpr] and the processing
|
|
result is the evaluation of the @racket[yields-expr] expression. The @racket[rendering-xexpr] form is a quasiquoted
|
|
syntactic @xexpr, with three special caveats:
|
|
|
|
@racket[,{_formlet-expr . => . _name}] embeds the
|
|
@tech{formlet} given by @racket[_formlet-expr]; the result of processing this formlet is
|
|
available in the @racket[yields-expr] as @racket[_name].
|
|
|
|
@racket[,{_formlet-expr . => . (values _name ...)}] embeds the
|
|
@tech{formlet} given by @racket[_formlet-expr]; the results of processing this formlet is
|
|
available in the @racket[yields-expr] as @racket[_name ...].
|
|
|
|
@racket[(#%# _xexpr ...)] renders an @xexpr forest.
|
|
|
|
These forms @emph{may not} appear nested inside @racket[unquote] or @racket[unquote-splicing]. For example, this is illegal:
|
|
@racketblock[
|
|
(formlet (div ,@(for/list ([i (in-range 10)])
|
|
`(p ,((text-input) . => . name))))
|
|
name)
|
|
]
|
|
}
|
|
|
|
@defidform[#%#]{Only allowed inside @racket[formlet] and @racket[formlet*].}
|
|
|
|
}
|
|
|
|
@section{Dynamic Syntactic Shorthand}
|
|
|
|
@(require (for-label web-server/formlets/dyn-syntax))
|
|
@defmodule[web-server/formlets/dyn-syntax]{
|
|
|
|
The @racket[formlet] syntax is too restrictive for some applications because it forces the @racket[_rendering]
|
|
to be @emph{syntactically} an @|xexpr|. You may discover you want to use a more "dynamic" shorthand.
|
|
|
|
@defform[(formlet* rendering-expr yields-expr)]{
|
|
Constructs a @tech{formlet} where @racket[rendering-expr] is evaluated (with caveats) to construct the rendering
|
|
and the processing result is the evaluation of the @racket[yields-expr] expression.
|
|
The @racket[rendering-expr] should evaluate to an "@xexpr" that may embed the results of the following forms
|
|
that only have meaning within @racket[formlet*]:
|
|
|
|
@racket[{_formlet-expr . =>* . _name}] embeds the
|
|
@tech{formlet} given by @racket[_formlet-expr]; the result of processing this formlet is
|
|
available in the @racket[yields-expr] as @racket[_name].
|
|
|
|
@racket[{_formlet-expr . =>* . (values _name ...)}] embeds the
|
|
@tech{formlet} given by @racket[_formlet-expr]; the results of processing this formlet is
|
|
available in the @racket[yields-expr] as @racket[_name ...].
|
|
|
|
@racket[(#%# _xexpr-expr ...)] renders an @xexpr forest.
|
|
|
|
Each of these forms evaluates to an opaque value that @racket[rendering-expr] may not manipulate in any way,
|
|
but if it is returned to @racket[formlet*] as part of an "@xexpr" it will be rendered and the formlets processing
|
|
stages will be executed, etc.
|
|
|
|
Because these forms @emph{may} appear anywhere in @racket[rendering-expr], they may be duplicated. Therefore,
|
|
the formlet may render (and be processed) multiple times. Thus, in @racket[yields-expr] the formlet result names are
|
|
bound to lists of results rather than single results as in @racket[formlet]. The result list is ordered according
|
|
to the order of the formlets in the result of @racket[rendering-expr]. For example, in
|
|
@racketblock[
|
|
(formlet* `(div ,@(for/list ([i (in-range 1 10)])
|
|
`(p ,(number->string i)
|
|
,((text-input) . =>* . name))))
|
|
name)
|
|
]
|
|
@racket[name] is bound to a list of strings, not a single string, where the first element is the string that
|
|
was inputted next to the string @litchar{1} on the Web page.
|
|
|
|
In this example, it is clear that this is the desired behavior. However, sometimes the value of a formlet's
|
|
result may be surprising. For example, in
|
|
@racketblock[
|
|
(formlet* `(div (p ,((text-input) . =>* . name)))
|
|
name)
|
|
]
|
|
@racket[name] is bound to a list of strings, because @racket[formlet*] cannot syntactically determine if
|
|
the formlet whose result is bound to @racket[name] is used many times.
|
|
|
|
}
|
|
|
|
@defidform[=>*]{Only allowed inside @racket[formlet*].}
|
|
|
|
}
|
|
|
|
@section{Functional Usage}
|
|
|
|
@(require (for-label web-server/formlets/lib))
|
|
@defmodule[web-server/formlets/lib]{
|
|
|
|
The syntactic shorthand abbreviates the construction of @tech{formlet}s with the following library.
|
|
These combinators may be used directly to construct low-level formlets, such as those for new INPUT element
|
|
types. Refer to @secref["input-formlets"] for example low-level formlets using these combinators.
|
|
|
|
@defthing[xexpr-forest/c contract?]{
|
|
Equivalent to @racket[(listof xexpr/c)]
|
|
}
|
|
|
|
@defproc[(formlet/c [content any/c] ...) contract?]{
|
|
Equivalent to @racket[(integer? . -> .
|
|
(values xexpr-forest/c
|
|
((listof binding?) . -> . (values (coerce-contract 'formlet/c content) ...))
|
|
integer?))].
|
|
|
|
A @tech{formlet}'s internal representation is a function from an initial input number
|
|
to an @xexpr forest rendering, a processing function, and the next allowable
|
|
input number.
|
|
}
|
|
|
|
@defthing[formlet*/c contract?]{
|
|
Equivalent to @racket[(formlet/c any/c ...)].
|
|
}
|
|
|
|
@defproc[(pure [value any/c]) (formlet/c any/c)]{
|
|
Constructs a @tech{formlet} that has no rendering and always returns @racket[value] in
|
|
the processing stage.
|
|
}
|
|
|
|
@defproc[(cross [f (formlet/c procedure?)]
|
|
[g (formlet/c any/c ...)])
|
|
(formlet/c any/c ...)]{
|
|
Constructs a @tech{formlet} with a rendering equal to the concatenation of the renderings of @tech{formlet}s @racket[f] and @racket[g];
|
|
a processing stage that applies @racket[g]'s processing results to @racket[f]'s processing result.
|
|
}
|
|
|
|
@defproc[(cross* [f (formlet/c (() () #:rest (listof any/c) . ->* . any/c))]
|
|
[g (formlet/c any/c)] ...)
|
|
(formlet/c any/c)]{
|
|
Equivalent to @racket[cross] lifted to many arguments.
|
|
}
|
|
|
|
@defproc[(xml-forest [r xexpr-forest/c])
|
|
(formlet/c procedure?)]{
|
|
Constructs a @tech{formlet} with the rendering @racket[r] and the
|
|
identity procedure as the processing step.
|
|
}
|
|
|
|
@defproc[(xml [r xexpr/c])
|
|
(formlet/c procedure?)]{
|
|
Equivalent to @racket[(xml-forest (list r))].
|
|
}
|
|
|
|
@defproc[(text [r string?])
|
|
(formlet/c procedure?)]{
|
|
Equivalent to @racket[(xml r)].
|
|
}
|
|
|
|
@defproc[(tag-xexpr [tag symbol?]
|
|
[attrs (listof (list/c symbol? string?))]
|
|
[inner (formlet/c any/c)])
|
|
(formlet/c any/c)]{
|
|
Constructs a @tech{formlet} with the rendering @racket[(list (list*
|
|
tag attrs inner-rendering))] where @racket[inner-rendering] is the
|
|
rendering of @racket[inner] and the processing stage identical to
|
|
@racket[inner].
|
|
}
|
|
|
|
@defproc[(formlet-display [f (formlet/c any/c)])
|
|
xexpr-forest/c]{
|
|
Renders @racket[f].
|
|
}
|
|
|
|
@defproc[(formlet-process [f (formlet/c any/c ...)]
|
|
[r request?])
|
|
(values any/c ...)]{
|
|
Runs the processing stage of @racket[f] on the bindings in @racket[r].
|
|
}
|
|
|
|
}
|
|
|
|
@section[#:tag "input-formlets"]{Predefined Formlets}
|
|
|
|
@(require (for-label web-server/formlets/input))
|
|
@defmodule[web-server/formlets/input]{
|
|
|
|
These @tech{formlet}s are the main combinators for form input.
|
|
|
|
@defproc[(make-input [render (string? . -> . xexpr/c)])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} is rendered with @racket[render], which is passed
|
|
the input name, and results in the extracted @racket[binding].
|
|
}
|
|
|
|
@defproc[(make-input* [render (string? . -> . xexpr/c)])
|
|
(formlet/c (listof binding?))]{
|
|
This @tech{formlet} is rendered with @racket[render], which is passed
|
|
the input name, and results in all the @racket[binding]s that use the
|
|
name.
|
|
}
|
|
|
|
@defproc[(text-input [#:value value (or/c false/c bytes?) #f]
|
|
[#:size size (or/c false/c exact-nonnegative-integer?) #f]
|
|
[#:max-length max-length (or/c false/c exact-nonnegative-integer?) #f]
|
|
[#:read-only? read-only? boolean? #f]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an INPUT element with the TEXT type
|
|
and the attributes given in the arguments.
|
|
}
|
|
|
|
@defproc[(password-input [#:value value (or/c false/c bytes?) #f]
|
|
[#:size size (or/c false/c exact-nonnegative-integer?) #f]
|
|
[#:max-length max-length (or/c false/c exact-nonnegative-integer?) #f]
|
|
[#:read-only? read-only? boolean? #f]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an INPUT element with the PASSWORD
|
|
type and the attributes given in the arguments.
|
|
}
|
|
|
|
@defproc[(textarea-input [#:value value (or/c false/c bytes?) #f]
|
|
[#:rows rows (or/c false/c number?) #f]
|
|
[#:cols cols (or/c false/c number?) #f]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an TEXTAREA element with attributes
|
|
given in the arguments.
|
|
}
|
|
|
|
@defproc[(checkbox [value bytes?]
|
|
[checked? boolean?]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an INPUT element with the CHECKBOX
|
|
type and the attributes given in the arguments.
|
|
}
|
|
|
|
@defproc[(radio [value bytes?]
|
|
[checked? boolean?]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an INPUT element with the RADIO type and the attributes given in the arguments.
|
|
}
|
|
|
|
@defproc[(radio-group [l sequence?]
|
|
[#:attributes attrs (any/c . -> . (listof (list/c symbol? string?))) (λ (x) empty)]
|
|
[#:checked? checked? (any/c . -> . boolean?) (λ (x) #f)]
|
|
[#:display display (any/c . -> . xexpr/c) (λ (x) x)])
|
|
(formlet/c any/c)]{
|
|
|
|
This @tech{formlet} renders using a sequence of INPUT elements of
|
|
RADIO type where each element gets its attributes from @racket[attrs]
|
|
that share a single NAME. An element is checked if @racket[checked?]
|
|
returns @racket[#t]. Elements are followed by the results of
|
|
@racket[display]. The result of processing this formlet is a single
|
|
element of the sequence.
|
|
}
|
|
|
|
@defproc[(checkbox-group [l sequence?]
|
|
[#:attributes attrs (any/c . -> . (listof (list/c symbol? string?))) (λ (x) empty)]
|
|
[#:checked? checked? (any/c . -> . boolean?) (λ (x) #f)]
|
|
[#:display display (any/c . -> . xexpr/c) (λ (x) x)])
|
|
(formlet/c (listof any/c))]{
|
|
|
|
This @tech{formlet} renders using a sequence of INPUT elements of
|
|
CHECKBOX type where each element gets its attributes from
|
|
@racket[attrs] that share a single NAME. An element is checked if
|
|
@racket[checked?] returns @racket[#t]. Elements are followed by the
|
|
results of @racket[display]. The result of processing this formlet is
|
|
a list of elements of the sequence.
|
|
}
|
|
|
|
@defproc[(submit [value bytes?]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an INPUT element with the SUBMIT
|
|
type and the attributes given in the arguments.
|
|
}
|
|
|
|
@defproc[(reset [value bytes?]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an INPUT element with the RESET type
|
|
and the attributes given in the arguments.
|
|
}
|
|
|
|
@defproc[(file-upload [#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an INPUT element with the FILE type
|
|
and the attributes given in the arguments.
|
|
}
|
|
|
|
@defproc[(hidden [value bytes?] [#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an INPUT element with HIDDEN type
|
|
and the attributes given in the arguments.
|
|
}
|
|
|
|
@defproc[(img [alt bytes?]
|
|
[src bytes?]
|
|
[#:height height (or/c false/c exact-nonnegative-integer?) #f]
|
|
[#:longdesc ldesc (or/c false/c bytes?) #f]
|
|
[#:usemap map (or/c false/c bytes?) #f]
|
|
[#:width width (or/c false/c exact-nonnegative-integer?) #f]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using an IMG element with the attributes
|
|
given in the arguments.
|
|
}
|
|
|
|
@defproc[(button [type bytes?]
|
|
[button-text bytes?]
|
|
[#:disabled disabled boolean? #f]
|
|
[#:value value (or/c false/c bytes?) #f]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty])
|
|
(formlet/c (or/c false/c binding?))]{
|
|
This @tech{formlet} renders using a BUTTON element with the attributes
|
|
given in the arguments. @racket[button-text] is the text that will
|
|
appear on the button when rendered.
|
|
}
|
|
|
|
@defproc[(multiselect-input [l sequence?]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty]
|
|
[#:multiple? multiple? boolean? #t]
|
|
[#:selected? selected? (any/c . -> . boolean?) (λ (x) #f)]
|
|
[#:display display (any/c . -> . xexpr/c) (λ (x) x)])
|
|
(formlet/c list?)]{
|
|
This @tech{formlet} renders using an SELECT element with the
|
|
attributes given with an OPTION for each element of the sequence. If
|
|
@racket[multiple?] is @racket[#t], then multiple options may be
|
|
selected. An element is selected if @racket[selected?] returns
|
|
@racket[#t]. Elements are displayed with @racket[display].
|
|
}
|
|
|
|
@defproc[(select-input [l sequence?]
|
|
[#:attributes attrs (listof (list/c symbol? string?)) empty]
|
|
[#:selected? selected? (any/c . -> . boolean?) (λ (x) #f)]
|
|
[#:display display (any/c . -> . xexpr/c) (λ (x) x)])
|
|
(formlet/c any/c)]{
|
|
This @tech{formlet} renders using an SELECT element with the
|
|
attributes given with an OPTION for each element of the sequence. An
|
|
element is selected if @racket[selected?] returns
|
|
@racket[#t]. Elements are displayed with @racket[display].
|
|
}
|
|
|
|
@defproc[(required [f (formlet/c (or/c false/c binding?))])
|
|
(formlet/c bytes?)]{
|
|
Constructs a @tech{formlet} that extracts the
|
|
@racket[binding:form-value] from the binding produced by @racket[f],
|
|
or errors.
|
|
}
|
|
|
|
@defproc[(default
|
|
[def bytes?]
|
|
[f (formlet/c (or/c false/c binding?))])
|
|
(formlet/c bytes?)]{
|
|
Constructs a @tech{formlet} that extracts the
|
|
@racket[binding:form-value] from the binding produced by @racket[f],
|
|
or returns @racket[def].
|
|
}
|
|
|
|
@defproc[(to-string [f (formlet/c bytes?)])
|
|
(formlet/c string?)]{
|
|
Converts @racket[f]'s output to a string. Equivalent to
|
|
@racket[(cross (pure bytes->string/utf-8) f)].
|
|
}
|
|
|
|
@defproc[(to-number [f (formlet/c string?)])
|
|
(formlet/c number?)]{
|
|
Converts @racket[f]'s output to a number. Equivalent to @racket[(cross
|
|
(pure string->number) f)].
|
|
}
|
|
|
|
@defproc[(to-symbol [f (formlet/c string?)])
|
|
(formlet/c symbol?)]{
|
|
Converts @racket[f]'s output to a symbol. Equivalent to
|
|
@racket[(cross (pure string->symbol) f)].
|
|
}
|
|
|
|
@defproc[(to-boolean [f (formlet/c bytes?)])
|
|
(formlet/c boolean?)]{
|
|
Converts @racket[f]'s output to a boolean, if it is equal to
|
|
@racket[#"on"].
|
|
}
|
|
|
|
@defthing[input-string (formlet/c string?)]{
|
|
Equivalent to @racket[(to-string (required (text-input)))].
|
|
}
|
|
|
|
@defthing[input-int (formlet/c integer?)]{
|
|
Equivalent to @racket[(to-number input-string)].
|
|
}
|
|
|
|
@defthing[input-symbol (formlet/c symbol?)]{
|
|
Equivalent to @racket[(to-symbol input-string)].
|
|
}
|
|
|
|
}
|
|
|
|
@section{Utilities}
|
|
|
|
@(require (for-label web-server/formlets/servlet
|
|
web-server/http))
|
|
@defmodule[web-server/formlets/servlet]{
|
|
|
|
A few utilities are provided for using @tech{formlet}s in Web applications.
|
|
|
|
@defproc[(send/formlet [f (formlet/c any/c ...)]
|
|
[#:method method
|
|
(or/c "GET" "POST" "get" "post")
|
|
"POST"]
|
|
[#:wrap wrapper
|
|
(xexpr/c . -> . xexpr/c)
|
|
(lambda (form-xexpr)
|
|
`(html (head (title "Form Entry"))
|
|
(body ,form-xexpr)))])
|
|
(values any/c ...)]{
|
|
|
|
Uses @racket[send/suspend] and @racket[response/xexpr] to send
|
|
@racket[f]'s rendering (wrapped in a FORM tag with method
|
|
@racket[method] whose action is the continuation URL (wrapped again
|
|
by @racket[wrapper])) to the client. When the form is submitted,
|
|
the request is passed to the processing stage of @racket[f].
|
|
|
|
}
|
|
|
|
@defproc[(embed-formlet [embed/url ((request? . -> . any) . -> . string?)]
|
|
[f (formlet/c any/c ...)])
|
|
xexpr/c]{
|
|
Like @racket[send/formlet], but for use with
|
|
@racket[send/suspend/dispatch].
|
|
}
|
|
|
|
}
|