Adding formlets to tutorial

This commit is contained in:
Jay McCarthy 2010-06-04 09:43:12 -06:00
parent 88a06c724c
commit 8e10ea0fef
2 changed files with 249 additions and 0 deletions

View File

@ -1122,6 +1122,110 @@ web-server/insta
....
]
@section{Using Formlets}
@(require (for-label web-server/formlets))
We'll now go back to the application code. One of the poor design choices we made earlier is the loose connection between the names of form elements in the display and in the form processing code:
@racketblock[
@code:comment{render-blog-page: blog request -> html-response}
@code:comment{Produces an html-response page of the content of the}
@code:comment{blog.}
(define (render-blog-page a-blog request)
(local [(define (response-generator make-url)
`(html (head (title "My Blog"))
(body
(h1 "My Blog")
,(render-posts a-blog make-url)
(form ((action
,(make-url insert-post-handler)))
@code:comment{"title" is used here}
(input ((name "title")))
(input ((name "body")))
(input ((type "submit")))))))
(define (insert-post-handler request)
(define bindings (request-bindings request))
(blog-insert-post!
a-blog
@code:comment{And "title" is used here.}
(extract-binding/single 'title bindings)
(extract-binding/single 'body bindings))
(render-blog-page a-blog (redirect/get)))]
(send/suspend/dispatch response-generator)))
]
@(define formlet
@tech[#:doc '(lib "web-server/scribblings/web-server.scrbl")]{formlet})
@(define formlets
@tech[#:doc '(lib "web-server/scribblings/web-server.scrbl")]{formlets})
The Racket Web framework provides @formlets to abstract both the display and the processing associated with a form, leaving the names in the HTML implicit.
Here is a @formlet for @racket[render-blog-page]:
@racketblock[
@code:comment{new-post-formlet : formlet (values string? string?)}
@code:comment{A formlet for requesting a title and body of a post}
(define new-post-formlet
(formlet
(#%# ,{input-string . => . title}
,{input-string . => . body})
(values title body)))
]
The first argument of @racket[formlet] is a quoted X-expression that will be rendered when the @formlet is, but with two caveats: @racket[#%#] represents a list of X-expressions and @racket[,{_formlet . => . _id}] embeds the @formlet @racket[_formlet] in this @formlet and names the result of its processing stage @racket[_id].
The second argument of @racket[formlet] is the processing stage of the @formlet where all the identifiers on the right-hand side of @racket[=>] are bound to the results of processing of the sub-@|formlets|.
There are two functions that operate on @|formlets|:
@itemize[
@item{@racket[formlet-display] takes a @formlet and returns its rendering as a list of X-expressions. This will generate unique names for its form elements.}
@item{@racket[formlet-process] takes a @formlet and a request and returns its processing. This automatically extracts the bindings from the request using the names generated by @racket[formlet-display].}
]
In @racket[new-post-formlet] we used @racket[input-string], a library @formlet that renders as @racketresult[`(input ([type "text"] [name ,_fresh_name]))] and is processed as @racket[(extract-binding/single _fresh_name (request-bindings request))]. Thus, @racket[(formlet-dispay new-post-formlet)] returns:
@racketresultblock[
(list '(input ([type "text"] [name "input_0"]))
'(input ([type "text"] [name "input_1"])))
]
And @racket[(formlet-process new-post-formlet _request)] where @racket[_request] binds @racketresult["input_0"] to @racketresult["Title"] and @racketresult["input_1"] to @racketresult["Body"] will return @racketresult[(values "Title" "Body")].
We can use @racket[new-post-formlet] in @racket[render-blog-page] as follows:
@racketblock[
@code:comment{render-blog-page: blog request -> html-response}
@code:comment{Produces an html-response page of the content of the}
@code:comment{blog.}
(define (render-blog-page a-blog request)
(local [(define (response-generator make-url)
`(html (head (title "My Blog"))
(body
(h1 "My Blog")
,(render-posts a-blog make-url)
(form ([action
,(make-url insert-post-handler)])
,@(formlet-display new-post-formlet)
(input ([type "submit"]))))))
(define (insert-post-handler request)
(define-values (title body) (formlet-process new-post-formlet request))
(blog-insert-post! a-blog title body)
(render-blog-page a-blog (redirect/get)))]
(send/suspend/dispatch response-generator)))
]
@bold{Exercise.} Write a @formlet and use it in @racket[render-post-detail-page].
@centerline{------------}
Our application is now:
@external-file["iteration-11.rkt"]
@section{Leaving DrRacket}
So far, to run our application, we've been pressing @onscreen{Run} in DrRacket. If we were to actually deploy

View File

@ -0,0 +1,145 @@
#lang web-server/insta
(require web-server/formlets
"model-3.rkt")
;; start: request -> html-response
;; Consumes a request and produces a page that displays
;; all of the web content.
(define (start request)
(render-blog-page
(initialize-blog!
(build-path (current-directory)
"the-blog-data.sqlite"))
request))
;; new-post-formlet : formlet (values string? string?)
;; A formlet for requesting a title and body of a post
(define new-post-formlet
(formlet
(#%# ,{input-string . => . title}
,{input-string . => . body})
(values title body)))
;; render-blog-page: blog request -> html-response
;; Produces an html-response page of the content of the
;; blog.
(define (render-blog-page a-blog request)
(local [(define (response-generator make-url)
`(html (head (title "My Blog"))
(body
(h1 "My Blog")
,(render-posts a-blog make-url)
(form ([action
,(make-url insert-post-handler)])
,@(formlet-display new-post-formlet)
(input ([type "submit"]))))))
(define (insert-post-handler request)
(define-values (title body)
(formlet-process new-post-formlet request))
(blog-insert-post! a-blog title body)
(render-blog-page a-blog (redirect/get)))]
(send/suspend/dispatch response-generator)))
;; new-comment-formlet : formlet string
;; A formlet for requesting a comment
(define new-comment-formlet
input-string)
;; render-post-detail-page: post request -> html-response
;; Consumes a post and produces a detail page of the post.
;; The user will be able to either insert new comments
;; or go back to render-blog-page.
(define (render-post-detail-page a-blog a-post request)
(local [(define (response-generator make-url)
`(html (head (title "Post Details"))
(body
(h1 "Post Details")
(h2 ,(post-title a-post))
(p ,(post-body a-post))
,(render-as-itemized-list
(post-comments a-post))
(form ([action
,(make-url insert-comment-handler)])
,@(formlet-display new-comment-formlet)
(input ([type "submit"])))
(a ([href ,(make-url back-handler)])
"Back to the blog"))))
(define (insert-comment-handler request)
(render-confirm-add-comment-page
a-blog
(formlet-process new-comment-formlet request)
a-post
request))
(define (back-handler request)
(render-blog-page a-blog request))]
(send/suspend/dispatch response-generator)))
;; render-confirm-add-comment-page :
;; blog comment post request -> html-response
;; Consumes a comment that we intend to add to a post, as well
;; as the request. If the user follows through, adds a comment
;; and goes back to the display page. Otherwise, goes back to
;; the detail page of the post.
(define (render-confirm-add-comment-page a-blog a-comment
a-post request)
(local [(define (response-generator make-url)
`(html (head (title "Add a Comment"))
(body
(h1 "Add a Comment")
"The comment: " (div (p ,a-comment))
"will be added to "
(div ,(post-title a-post))
(p (a ([href ,(make-url yes-handler)])
"Yes, add the comment."))
(p (a ([href ,(make-url cancel-handler)])
"No, I changed my mind!")))))
(define (yes-handler request)
(post-insert-comment! a-blog a-post a-comment)
(render-post-detail-page a-blog a-post (redirect/get)))
(define (cancel-handler request)
(render-post-detail-page a-blog a-post request))]
(send/suspend/dispatch response-generator)))
;; render-post: post (handler -> string) -> html-response
;; Consumes a post, produces an html-response fragment of the post.
;; The fragment contains a link to show a detailed view of the post.
(define (render-post a-blog a-post make-url)
(local [(define (view-post-handler request)
(render-post-detail-page a-blog a-post request))]
`(div ([class "post"])
(a ([href ,(make-url view-post-handler)])
,(post-title a-post))
(p ,(post-body a-post))
(div ,(number->string (length (post-comments a-post)))
" comment(s)"))))
;; render-posts: blog (handler -> string) -> html-response
;; Consumes a make-url, produces an html-response fragment
;; of all its posts.
(define (render-posts a-blog make-url)
(local [(define (render-post/make-url a-post)
(render-post a-blog a-post make-url))]
`(div ([class "posts"])
,@(map render-post/make-url (blog-posts a-blog)))))
;; render-as-itemized-list: (listof html-response) -> html-response
;; Consumes a list of items, and produces a rendering as
;; an unorderered list.
(define (render-as-itemized-list fragments)
`(ul ,@(map render-as-item fragments)))
;; render-as-item: html-response -> html-response
;; Consumes an html-response, and produces a rendering
;; as a list item.
(define (render-as-item a-fragment)
`(li ,a-fragment))