This post aims to answer some of the questions I originally struggled with when I started using RequestVar's. I think I've got to grips with them sufficiently to help others who are wrestling with the same issue :-). So, here's my contribution to the Lift corpus ...
Consider the common use case, whereby when the user submits a form, if the data fails validation, the data entered is re-presented to the user for correction. The following common pattern meets this requirement by using a RequestVar to hold the state of the submitted data between requests.
The snippet:
class NameSnippet { object nameRV extends RequestVar("") // 1 def processSubmit() = { // perform some simple validation if (nameRV.is.length < 3) S.error("event name must be at least 3 characters") else { S.notice("name OK") S.redirectTo("/") } } def render = { "#name" #> SHtml.text(nameRV.is, x => nameRV(x) ) & // 2 "#submit" #> SHtml.onSubmitUnit(processSubmit) // 3 } }
The HTML template, name.html:
<div data-lift="lift:surround?with=default;at=content"> <div data-lift="NameSnippet?form=post"> <!-- 4 --> Name: <input id="name" type="text"/><br /> <input id="submit" type="submit"/> <br /> </div> </div>
When I first came across this use of RequestVar, I struggled to understand what the RequestVar gives us that we wouldn't get from using a regular var: what was going on in line (2) with closures and scope - weren't the name variables being set and read in different scopes? How could they be communicating?
To help answer these questions, I'll break the processing chain down to the basic steps:
-
The user types in the url of our web form, http://localhost:8080/name, say, sending an initial HTTP GET request to the server.
-
Having resolved the request to the name.html template, Lift creates an instance of the NameSnippet class and calls the 'render' method to transform the contents of the HTML template node at line (4).
-
In 'render', at line (2), Lift transforms the content of name input node in our HTML into something like:
<input id="name" name="F1235075760726VICVPV" type="text" value=""/>
The 'value' attribute is set from calling nameRV.is. This is the first time nameRV is accessed and so Scala initialises the nameRV object at this point (remember Scala only initialises objects when they are first accessed).
As we are calling the 'is' method before nameRV has been set, before Lift returns us a value, it sets nameRV to contain the default value we specified at (1), namely the empty string.
Lift also keeps a record of the anonymous function 'x => nameRV(x)', which it will call later when the user submits the form. The name="F1235075760726VICVPV" attribute is Lift's mechanism for identifying which function is to be called. It will be called with the value the user enters in the Name input field. This function creates a closure around the nameRV object in the scope of the NameSnippet instance used to service this initial request.
-
At line (3), Lift transforms the content of the 'submit' input node into something like:
<input id="submit" name="F1235075860727PTINTI" type="submit" />
As above, Lift registers the processSubmit function with the name F1235075860727PTINTI which it will call when the user submits the form. Again, processSubmit is closing around the nameRV object in the scope of the NameSnippet created to service this initial request.
-
When the user enters data into the name input field (say 'Jo') & clicks 'submit', a second request (this time a POST) is made to http://localhost:8080/name, sending the following key-value pairs in the message body:
F1235075760726VICVPV:Jo F1235075860727PTINTI:Submit
Lift recognises the two keys as being function identifiers, and looks up and calls the functions in the order in which they are received. Thus first, 'x => nameRV(x)' is called to set nameRV with the contents of the name input field. But remember, nameRV is the instance that was created in the scope of the first NameSnippet instance. Second, processSubmit is called, which, in the body of the method, reads the content of nameRV we have just set. Again, this is the nameRV in the scope of the first request.
-
Suppose that the user had entered a name that passed validation (i.e. was more than three characters); processSubmit would then call redirectTo("/"), which throws an exception to interrupt the flow of control, taking the user to "/". But in this example, the input name 'Jo' fails validation, and processSubmit exits normally and Lift carries on loading the input form, name.html.
-
To service this second request, Lift creates a second instance of NameSnippet and calls NameSnippet.render again. This time, when nameRV.is is called at (2), the nameRV.is call returns "Jo" and the user consequently sees his input being re-presented to him, allowing him to correct the error.
But, how does this nameRV.is return 'Jo', when this nameRV object is in the scope of the second request, but the nameRV that was set was in the scope of the first request?
The answer lies in realising that there is lot more going on behind the scenes with RequestVar's than we might believe from their apparently simple usage.
RequestVar's are actually acting as indices into a backing datastore: in this sense they are not acting as 'regular' variables (with normal scoping rules etc.) as the 'Var' in the name might suggest. When you set a RequestVar with something like nameRV("Jo"), the set method creates an entry in a map, where the 'key' is based on the name of the variable (in this case, something like liftsvcode.snippet.NameSnippet$nameRV$), and 'value' is what we're setting the RequestVar to (in this case 'Jo').
This backing datastore is shared across all snippets and closures called during the processing a single HTTP request (and actually all Ajax calls arising from this request, but that's outside the scope of this post). Thus, even though the nameRV object on which the set method is called is a different instance from the one that is read, they are setting and reading the same datastore value. That is because they both use the same name as the key into the same datastore. So, in this sense, RequestVar's are acting as variables that are accessible across scope boundaries.
So, to answer the question of 'What does RequesVar's give us that regular var's don't?', if we tried to write our snippet with a regular var instead of a RequestVar, with something like:
class NameSnippet { var name = String("") // 1 def processSubmit() = { .... } def render = { "#name" #> SHtml.text(name, x => {name = x} ) & // 2 "#submit" #> SHtml.onSubmitUnit(processSubmit) // 3 } }
... the closure that sets the name 'x => {name = x}' would be setting a different variable from the one that is being read with ' "#name" #> SHtml.text(name, ... ' . Thus when the form validation fails and the form is rendered for a second time, the name field would be blank; not what we want!
I'll be writing another post soon describing more advanced patterns using RequestVar's, so watch this space ...
Please feel free to leave comments / questions etc.