DOM scripting in Domino... on the server?
Something that I've been chipping away at for a while, and is now beginning to take some shape, is a framework that uses NotesDOMParser to construct HTML output using the server-side equivalent of DOM scripting in JavaScript. I guess you could consider this sort of a GWT for LotusScript. If you've done any serious JavaScript development, you're by now familiar with DOM scripting: the art of manipulating an HTML document by treating it like a tree of "nodes", and appending, modifying, and removing nodes to alter the content, presentation, and behavior of the document (uh oh...that's all three tiers in one layer... but this is "Web 2.0" now, so apparently all bets are off). Well, for five years now, Notes and Domino have included a similar model, embodied in the LotusScript NotesDOMParser class.
A DOM document can be any XML document... don't let that scare you: I've often heard developers say, "I know HTML, but I don't know XML". Yes, you do. HTML is XML. It's just a specific flavor of XML. So is DXL (my personal favorite, in fact, which will surprise none of my regular readers). A dachshund is a dog, HTML is XML. There are various reasons why this distinction is often overlooked, but I think the most common is that browsers don't currently enforce all the rules that ordinarily apply to XML when rendering HTML... unless the document declares that it conforms to the "XHTML Strict" DTD, which is essentially just an assertion that the HTML obeys all the XML rules, as well as additional rules that define what is "valid" HTML. If browsers forced all HTML to be valid XHTML, I suspect that every web developer would be painfully aware that HTML is XML.
The shortcomings of Domino's default rendering of HTML are "well-known and often-lamented", so I won't reiterate all of the common criticisms here. But one I see quite frequently is that the output isn't strict XHTML. For that and other reasons, it's gotten to the point that it's often easier to just override Domino's output entirely and generate your own. Although my own web development tends to use Page design elements to define the HTML, using computed text to fill in the dynamic portions, another angle is to rely on agents to generate (and/or supplement) the output. I've seen various approaches to this, one of the most impressive of which is outlined in the last few posts by Michel Van der Meiren. I've even seen - though I'm not sure I'd recommend this - entire Domino applications that consisted only of agents. It's easy to do: one agent to locate documents and display them individually or in groups (you technically don't even need views, you can use db.Search / .FTSearch for queries... if you don't mind the performance hit and lack of sorting), another to post to when documents are created, modified, or deleted. Of course, that's a bit much, since we've then discarded everything that Domino does do well. In any case, since I'm seeing the concept of seizing control referred to more and more often as "rolling your own", I'm calling my new approach to this "RYOML" ("Roll Your Own Markup Language"); the intent is to mold this over time to be applicable not only to HTML, but to other flavors of XML as well, particularly DXL.
The primary advantage I see in taking a DOM approach to markup generation - as opposed to just a String or NotesStream - is that it allows you to get a handle on any node: html, head, body, script, div, doesn't matter... every node is (at least, temporarily) a distinct object reference. A 1-to-1 node-to-object relationship may not seem all that useful, but if you're already choosing to roll your own, you're probably at least a tiny little bit of a control freak, and this relationship gives you a ridiculous amount of control when compared to a single String handle. One benefit of this is that you can generate the content entirely out of sequence if that turns out to be more convenient. You can establish a handle on, say, a menu div, a header div, footer div, etc., and add content to any of them in any order. Okay, bad example. How about a separate table for each of a dozen different categories... if you add the tables to the page in order, it doesn't matter what order you find their content in, because you have a handle on each. So as your agent identifies additional table content, you determine which table it should go in and append it. For a very basic example of this, check out this page: after establishing a handle on 5 tables, the agent runs through a loop 40 times, each time selecting randomly which table to update (so each time you reload the page, you'd see different table content). In a real application your data would most likely be more predictable, but this example illustrates that, even if the data is completely random each time, you can still put that data precisely where you want it, because you have a separate handle on each node. This provides so much flexibility in how you structure the logic for constructing your page.
A secondary advantage is that it allows you to "forget" some of the XMLishness of HTML with impunity. You don't have to remember to put in closing tags... the DOM parser does that for you. You don't have to remember to put a closing slash in a tag that doesn't have a closing tag... the parser does that for you. Where necessary, it even sets certain tag attributes for you automatically. You just tell it the tag/attribute name, and the parser does the rest.
The downside is that DOM manipulation (both in JavaScript and in LotusScript) is extremely verbose. Although an agent can send the content of an entire web page with a single Print statement (two statements, if you want to send a Content-type header first), generating that content via a DOM can require dozens - or even hundreds, for extremely complex content - of lines of code. ROYML encapsulates a lot of the tedious aspects of DOM manipulation by setting up the initial parser input and output, referencing a Boolean in the constructor to determine whether the strict or transitional DTD will be used, parsing HTML fragments into a DOM sub-tree, and providing syntactical sugar like method chaining and one-line appending of new nodes with passed or default attribute values. The fragment parsing is what saves the most time, as it allows you to rapidly generate the bulk of the document, switching to manual only when you need a handle on a specific node for later. By the way, a HUGE thanks to Rocky for his CrossDocClone function, which made this piece possible; NotesDOMParser can't append nodes from one document into another, and this function provides a great way of compensating for that limitation. Ironically, this function also helped immensely in adding an insertBefore method to the framework.
Click "Read More" (unless you've already linked to the full article) to see the source code for the example page mentioned above. This agent is also included in the RYOML download database.
Random.html:
Sub Initialize
Dim myHtml As RyoHtml
Dim main As RyoNode, table As RyoNode, tableBodies List As RyoNode
Dim randomId As Integer, rowCount List As Integer, tableId As Integer
Dim tableLabel As String
Dim randomTableId As Variant
On Error Goto errorHandler
'True sets output to strict, False to transitional
Set myHtml = New RyoHtml(True)
'Add a meta tag
Call myHtml.appendMeta("publisher", "(c) Tim Tripcony","")
'Add a CSS link tag
Call myHtml.appendStyle("extjs/css/ext-all.css","","")
'Set an ID to allow JavaScript to easily change the skin later
Call myHtml.appendStyle("extjs/css/ytheme-vista.css","","skinSheet")
'Add a style tag to set CSS inline (URL parameter is ignored if inline rules are passed)
Call myHtml.appendStyle("",|* {font-family: arial; font-size: 9pt;}
table {border: 1px solid black; float: left; margin: 5px;}
caption {text-align: center; font-weight: bold; font-size: 12pt;}
td, th {border: 1px solid #bbb; padding: 2px;}
th {font-weight: bold;}
.random {background-color: #eee;}|,"inlineStyles")
'Grab a handle on a div to add content to later
Set main = myHtml.appendChild(myHtml.getBody(),"div","","main")
For tableId = 65 To 69 ' A through E
Let tableLabel = Chr(tableId)
'Add a new table to the div we created earlier
Set table = myHtml.appendContent(main,|<table id="randomTable| & tableLabel & |">
<caption>| & tableLabel & |</caption>
<thead>
<tr><th>Row ID</th><th>Random ID</th></tr>
</thead>
</table>|)
'Store the tbody tags to add content to later
Set tableBodies(tableLabel) = myHtml.appendChild(table, "tbody","","")
Let rowCount(tableLabel) = 0
Next
For randomId = 1 To 40 'Runs surprisingly fast
'Pick a random table to update
Let randomTableId = Evaluate({4 * @Random + 65})
Let tableLabel = Chr(randomTableId(0))
'Determine how many times the randomly chosen table has been updated
Let rowCount(tableLabel) = rowCount(tableLabel) + 1
'Add a row listing the number of times this table has been updated and which random iteration we're in
Call myHtml.appendContent(tableBodies(tableLabel),|<tr>
<td>| & Cstr(rowCount(tableLabel)) & |</td>
<td>| & Cstr(randomId) & |</td>
</tr>|).addClass("random") 'Many of the framework methods are chainable
Next
' To append a script tag to the end of the <body/> (omitted in this example):
' Call myHtml.appendScript("ext.js","",myHtml.getBody(),"extScript")
Call myHtml.render() 'Prints both the Content-type header and the final markup
Exit Sub
errorHandler:
Call LogError()
Exit Sub
End Sub
Comments
Oh, and spam all ya want... at least it's worthwhile info and not Cialis adverts.
Posted by Tim Tripcony At 06:13:56 PM On 09/30/2007 | - Website - |
What I do in my demo/prototype, is already more or less doable with a form/content-type html/xml/text.
Posted by Tommy Valand At 12:37:38 AM On 10/01/2007 | - Website - |
{ Link }
(sorry for spamming)
Posted by Tommy Valand At 12:34:05 PM On 09/30/2007 | - Website - |
I'm currently a little too blinded by excitement about my implementation to know if it is usable. I -think- it will make it simpler to deliver content in different content-types (xml, html, etc), but as previously stated, I may be blind :D
Instead of making one agent per template, I use an agent to render a content into a template stored in a NotesDocument.
I hope to finish the demoapp today or tuesday. It will be my next SNTT.
In it's unpolished state, it allows for stored (in NotesDocument) modules (think menus, etc), formula-snippets (@WebDbName, @UserName, etc.), and of course, referencing fields.
Posted by Tommy Valand At 10:12:57 AM On 09/30/2007 | - Website - |