Monday, August 24, 2009

Handling UI Chromes - Make it Switchable

You might have noticed that since topfilter gets executed with every single request, it is a nice place to specify the UI chrome so we do not need to separately specify it for every page.

The drawback with such approach is that once you have placed the chrome into topfilter, it'll accompany all of the responses (unless you raise an error or a response object to escape the continuation), regardless whether you intend it or not.

In majority of the cases, you'll be just fine, since most of the web pages would want to have such a chrome. But what if you are generating non-html pages? For example, if you want to dynamically generate a javascript file, or if you want to return an XML data (i.e. setting up a web-service). In these situation, a default chrome that runs 100% of the time defeats the purpose. Hence we want to have a "switchable" chrome that is defaulted to some value most of the time, but can be turned off as necessary.

The first thing we need again is a parameter:

(define $chrome (make-parameter #f))

What should go into the parameter is the path of the chrome file.

Then in order to execute the chrome, we'll wrap around the inner handler with:

(define (make-chrome-based-handler inner)
(lambda ()
(let ((result (inner)))
(if ($chrome)
(include! ($chrome) result)
result))))


(define (include! path
#:topfilter (topfilter #f)
#:partial? (partial? #f)
. args)
(define (make-script path partial?)
(evaluate-script (if (script? path)
(script-path path)
(resolve-path path partial?))))
(define (helper topfilter)
(let ((proc (make-script path partial?)))
(let ((chrome-handler (make-chrome-based-handler (lambda () (apply proc args)))))
(if topfilter
(topfilter chrome-handler)
(chrome-handler)))
))
(helper (if topfilter (make-script topfilter #f) topfilter)))
;; adding the chrome parameter
(define-struct shp-handler (path default not-found required topfilter chrome)
#:property prop:procedure
(lambda ($struct request)
(handle-request $struct request)))
;; parameterize the chrome parameter
(define (handle-request server request)
(parameterize (($server server) ...)
(parameterize (...
($chrome (shp-handler-chrome server)))
...)))

With the above, we can then put chrome into its own file:

;; a default chrome
(:args inner)
`(html (body ,inner)) ;; NOTE inner is not a procedure

You'll notice that chrome looks a lot like the topfilter. And yes, in a way, chrome is a specialized filter, but it differ from the topfilter in that the inner is not a procedure call. Instead, the values are first evaluated and finally pass to chrome. This way we can modify the $chrome parameter to disable it half way in the code.

So - when you use chrome you will need to remember that it is evaluated last. This generally should not matter, but it does when your code have order dependencies. Make sure your chrome will work as the last thing being executed in your code.

Productionize the Chrome Code

The above chrome code has a serious bug - it causes infinite recursion. The reason is that make-chrome-base-handler calls include! and include! calls make-chrome-base-handler, so we cannot use include! inside make-chrome-base-handler:

(define (make-chrome-based-handler inner)
(lambda ()
(let ((result (inner)))
(if ($chrome)
;; I see - chrome itself now will cause a recursion!!
((evaluate-script (resolve-path ($chrome) #f)) result)
result))))

With the above, your handler will return, but if you use include! in your script you'll notice they also have the chrome applied to them - our include! does not know when chrome should be applied (only at the outermost layer), so we'll also need to fix that:

(define (include! path
#:topfilter (topfilter #f)
#:partial? (partial? #f)
#:chrome? (chrome? #f) ;; disable by default.
. args)
(define (make-script path partial?)
(evaluate-script (if (script? path)
(script-path path)
(resolve-path path partial?))))
(define (helper topfilter)
(let ((proc (make-script path partial?)))
(let ((handler
(let ((handler (lambda () (apply proc args))))
(if chrome? (make-chrome-based-handler handler) handler))))
(if topfilter
(topfilter handler)
(handler))))
)
(helper (if topfilter (make-script topfilter #f) topfilter)))

And we need to ensure the outermost layer is called with chrome enabled:

(define (handle-request server request)
...
(make-response (include! (request-uri request)
#:topfilter (shp-handler-topfilter server)
#:chrome? #t
#:partial? #t))))))

Now we have a default and switchable chrome, and to turn the chrome off, just call ($chrome #f).

No comments:

Post a Comment