In this installment, we examine the new "comment preview" feature of the OPS Blog sample application.
Most blog applications' comment pages work this way: you see the text of the blog post, followed by a series of existing comments to the article and a text area which you use to enter a new comment. When you are done writing your comment, you have two possibilities: preview or directly post the comment. The purpose of the preview is to allow you to see what your comment will look like once posted, without actually submitting it. This is useful because most comment editors are not WYSIWYG: they often allow you to enter some special markup, usually inspired by HTML, to format text, add hyperlinks, etc. Surprisingly, you still find blog applications without this feature, which is annoying as you may end up posting garbage if you do not master the particular blog application's formatting markup.
Here is the good news: with the advent of Ajax, used by the OPS XForms engine, the preview functionality doesn't actually need a "Preview" button that reloads entirely the comments page: you can very easily implement a dynamic preview updated as the user types. How does this work?
In XForms, you bind controls to nodes of an XML document called an XForms instance. For example, the following XForms instance can contain the name of an author and the actual text of the comment:
<comment> <author>Erik B.</author> <text>This blog post rocks!</text> </comment>
text nodes are easily bound to XForms controls:
Now from there, displaying the comment outside the text area as it is entered simply consists in binding some
xforms:output controls to the same nodes:
<p> Comment written by: <xforms:output ref="author"/> </p> <p> Text of the comment: <xforms:output ref="text"/> </p>
Presto! As you type, the text of the comment appears within HTML paragraphs. Note that to achieve this, we had to use the attribute
xforms:textarea, in order to ensure that the XForms engine frequently perform updates as the user types. Without this attribute, the
xforms:output controls would only update upon the user changing focus between controls.
This is not the end of the story. Writing plain text comments is good, but we want to be able to use some markup too. First, we need to choose a format. The easiest way to do this is to choose a very small subset of HTML, using for example the
a tags. What happens if the user types those in the text area? Unfortunately, the output will actually contain the tags as well, and no HTML formatting will take place. This happens because the
xforms:output control, by default, outputs plain text, including opening and closing brackets.
How do you get the
xforms:output control to output HTML then? There is currently no standard to achieve this in XForms. OPS has chosen to use the
<p> Text of the comment: <xforms:output ref="text"/ appearance="xforms:xhtml"/> </p>
Type HTML markup in the text area, and your text will appear formatted with that markup in the preview section!
Adding an XML Service
xforms:bind with a
constraint attribute containing an XPath 2.0 regular expression function. Another solution is to delegate the validation of comments to server-side code. How do you do this? With the
replace="instance" feature of the
xforms:submission element this way allows you to use XForms to call XML services. An XML service is an online services that you call by sending it a request containing XML, and from which you receive an XML response. This is a powerful yet easy to use feature of XForms. What kind of service do we need for comment preview? One that takes a raw comment containing user markup, and returns an indication of whether the comment is valid or not and possibly, a cleaned-up version of the comment with extra markup added or filtered. We can do this with two XForms instances, and an adequate
<xforms:instance id="comment-request"> <comment> <text/> </comment> </xforms:instance> <xforms:instance id="comment-response"> <comment> <text/> </comment> </xforms:instance> <xforms:submission id="format-comment-submission" method="post" action="/blog/format-comment" ref="instance('comment-request')" replace="instance" instance="comment-response"/>
How do we use the above submission? We simply detect
xforms-value-change events sent by the text area control. Once this happens, we execute an event handler (declaratively, as always with XForms) which sends the
format-comment-submission submission. Nodes of the
comment-request instance are bound to the text area control, and nodes in the
comment-response instance are bound to the
xforms:output displaying HTML:
<xforms:textarea ref="comment/text" incremental="true"> <xforms:action ev:event="xforms-value-changed"> <xforms:send submission="format-comment-submission"/> </xforms:action> </xforms:textarea>
On the server, we must now react to the
/blog/format-comment action. To do this, we write an XPL file responding to that path:
The XPL file is here implemented as a page view. This way, the XPL pipeline can simply output its XML response on its
data output, and the default Page Flow Epilogue takes care of serialization, the process of sending the XML back to the web browser. It would have also been possible to implement it as a
model pipeline, but then it would have had to take care of XML serialization itself.
The XPL pipeline receives an XML document on its
instance input, which is simply the
comment-request instance submitted by the XForms engine. Then it calls an XSLT stylesheet that:
Parses the comment as an XML fragment using the
Filters out all disallowed elements and attributes
The stylesheet either fails or returns a cleaned-up version of the user comment, which the XForms engine uses to replace the
comment-response instance. In case of failure, we return an error document to the caller.
We are almost done with the preview feature! The last thing we would like to do now is mark the text area as invalid if the server-side code tells us that it is invalid. We can use the
xforms:bind element with a
constraint attribute, containing an XPath expression, to achieve this. But what should this XPath expression contain then? For example if it contains
false(), the test area is always going to be marked as invalid. If it contains
true(), it will always be marked as valid (unless some other constraint makes it invalid). We need to be smart and use the XForms submission response document in
comment-response. Here, we decide on a convention: if the response does not contain any text under
/comment/text, then we consider that the server is telling us the comment is invalid. The constraint is expressed as follows:
There is a little problem with this: if the server tells us that the text area is now invalid, we can no longer submit the
comment-request instance, because an instance but be valid to be submitted! So we need to add a third XForms instance, which is used strictly for the input and text area controls:
<xforms:instance id="main"> <form> <comment> <name/> <text/> </comment> </form> </xforms:instance>
When we catch value change events, we copy the value from the
main instance to the
comment-request instance first, using the
xforms:setvalue action. The text area element now looks like this:
<xforms:textarea ref="comment/text" incremental="true" xhtml:rows="10" xhtml:cols="80"> <xforms:action ev:event="xforms-value-changed"> <xforms:setvalue ref="instance('comment-request')/text" value="instance('main')/comment/text"/> <xforms:send submission="format-comment-submission"/> </xforms:action> </xforms:textarea>
So we end up with three XForms instance:
main: controls requiring user input are bound to this instance
comment-request: this is the XML document to send to the XML service to validate comments
comment-response: this is the XML document returned by the XML service that validates comments
In the general case, it makes sense to separate the request and response documents from the main XForms instance.
That is it! The code is currently available in CVS and the nightly builds, with some additions such as a "comment date" field, but the ideas above remain valid.