'DOM' Version 0.3

by Paul Murrell http://orcid.org/0000-0002-3224-8858

Thursday 20 October 2016


Creative Commons License
'DOM' version 0.3 by Paul Murrell is licensed under a Creative Commons Attribution 4.0 International License.


This report describes changes in version 0.3 of the 'DOM' package for R. This version represents a major refactoring of the package code, including its user-facing API. These changes were made in order to facilitate the addition of new features to the package, which in this version include: a new way to refer to DOM nodes from R code that allows building web page content "off screen"; and greater flexibility in how requests are made from R to a web browser and vice versa.

Changes to the DOM API

  library("DOM")

Parts of the 'DOM' package have been refactored to make use of S4 classes and generics so that the package API can become simpler and easier to extend.

For example, in the previous version of the 'DOM' pacakge (version 0.2), the appendChild function provided three ways to specify the "child" to append: as HTML code via the child argument; as CSS via the childRef argument; or as XPath via the childRef argument, with the css argument set to FALSE.

appendChild(pageID, child = NULL, childRef = NULL, parentRef = "body",
            ns = NULL, css = TRUE, async = !is.null(callback),
            callback = NULL, tag = getRequestID())
  

In addition, there was an appendChildCSS variant of the appendChild function, which did exactly the same thing, but returned its result as CSS rather than HTML.

In version 0.3, the "child" to append is now specified as an S4 object, the appendChild function is an S4 generic, and its result is an S4 object. This means that the argument list for appendChild has become smaller and the appendChildCSS function has disappeared.

Examples

The following code starts with a blank web page and adds a paragraph of text. The important change in this example is that the new child element is specified using the htmlNode function. This makes it clear that we are adding a new HTML node to the web page. The return value is also a DOM_node_HTML object, indicating that the browser has responded with an HTML representation of the child that was appended.

page <- htmlPage()
appendChild(page,
          child=htmlNode("<p>Paragraph 1</p>"))
  An object of class "DOM_node_HTML"
  [1] "<p>Paragraph 1</p>"

The next example adds a <span> element to the paragraph. This shows the use of the css function to make it clear that we are specifying the parent element for this append via a CSS selector (the argument is now called just parent rather than parentRef and there is no need for the old css argument).

appendChild(page,
          child=htmlNode('<span style="font-style: italic"> span 1</span>'),
          parent=css("p"))
  An object of class "DOM_node_HTML"
  [1] "<span style=\"font-style: italic\"> span 1</span>"

The next example adds another paragraph to the page, this time using the new response argument to specify that we want the return value as a CSS selector (rather than the default HTML). This means there is no more need for a separate appendChildCSS function. Notice that the return value is a DOM_node_CSS object, indicating that the browser has responded with a CSS selector for the child that was added.

appendChild(page,
          child=htmlNode("<p>Paragraph 2</p>"),
          response=css())
  An object of class "DOM_node_CSS"
  [1] "body > :nth-child(2)"

The next example moves the first paragraph to the end of the page (it appends an existing element rather than a new one). This demonstrates the new xpath function, which makes it clear that we are specifying the child to append via an XPath expression. This shows that the new appendChild argument list has dropped both the childRef argument and the css argument.

appendChild(page,
          child=xpath("//p[1]"))
  An object of class "DOM_node_HTML"
  [1] "<p>Paragraph 1<span style=\"font-style: italic\"> span 1</span></p>"

The next example moves the <span> element from paragraph 1 to paragraph 2. The significance of this example is that we are specifying the child with an XPath expression and the parent with a CSS selector. This mixing of XPath and CSS was not possible in the previous version of the appendChild function.

appendChild(page,
          child=xpath("//span[1]"),
          parent=css("p"))
  An object of class "DOM_node_HTML"
  [1] "<span style=\"font-style: italic\"> span 1</span>"

The next example shows another minor advantage of the new API: the ability to return results from a DOM request in the form of XPath expressions (in addition to HTML and CSS representations). The return result this time is a DOM_node_XPath object.

appendChild(page,
          child=htmlNode("<p>Paragraph 3</p>"),
          response=xpath())
  An object of class "DOM_node_XPath"
  [1] "/html[1]/body[1]/p[3]"

The next example adds some JavaScript to the web page, which both adds a <script> element to the page and runs the JavaScript code (so that the first paragraph on the page turns red). This demonstrates the new javascript function, which can be used to make clear that we are adding JavaScript code (the JavaScript code is automatically wrapped within a <script> element). This removes the need for the old appendScript function.

appendChild(page,
          child=javascript('document.getElementsByTagName("p")[0].setAttribute("style", "color: red")'))
  An object of class "DOM_node_HTML"
  [1] "<script>document.getElementsByTagName(\"p\")[0].setAttribute(\"style\", \"color: red\")</script>"

The next example adds an SVG image to the web page. This demonstrates the new svgNode function to make clear that we are adding SVG code to the page. It also demonstrates that the ns argument is now a boolean (rather than character) value, which indicates whether namespaces should be used when creating the new SVG content in the web page. Finally, the svgNode function is also used to specify that we want the return value as SVG code (the return value is a DOM_node_SVG object). The ns setting is important for both the generation of the SVG elements within the web page and for the generation of the SVG code in the return value.

appendChild(page,
          child=svgNode('<svg xmlns="http://www.w3.org/2000/svg" height="50"><circle r="50"/></svg>'),
          ns=TRUE,
          response=svgNode())
  An object of class "DOM_node_SVG"
  [1] "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"50\"><circle r=\"50\"/></svg>"

Changes to browser requests

In addition to the changes to the DOM API calls, which can be used to make requests from R to a web browser, there have also been changes to the JavaScript function RDOM.RCall, which is used to make requests from the web browser back to R.

In previous versions of the 'DOM' package, the RDOM.Rcall function took three arguments: the R function to call; a DOM node; and a callback. The R function was called with two arguments: an HTML representation of the DOM node; and a CSS selector for the DOM node.

RDOM.Rcall(fn, element, callback)
  

In version 0.3, the RDOM.Rcall function now takes four arguments: the R function to call; an array of DOM elements (or a single DOM element); an array of representations (one or more of "HTML", "SVG", "CSS", "XPath", and "ptr"); and a callback. The R function is called with a variable number of arguments equal to the number of DOM elements times the number of representations.

The example below shows the simplest case. The R function is called demo and it sets a global clickResult to record the arguments that it was called with. The call to setAttribute adds an onclick attribute to the <span> element on the web page so that an RDOM.Rcall call is made when the span is clicked. RDOM.Rcall calls the demo R function with a single argument, which is an HTML representation of the <span> element.

demo <- function(...) clickResult <<- list(...)
setAttribute(page,
           elt=css("span"),
           "onclick",
           'RDOM.Rcall("demo", this, [ "HTML" ], null)')

We can call the click function to simulate a click on the <span> element ...

click(page, elt=css("span"))

... and we can look at the global clickResult to see the arguments that were passed to the demo function (in this case, a single argument, which is the HTML code for the <span> element).

clickResult
  [[1]]
  An object of class "DOM_node_HTML"
  [1] "<span style=\"font-style: italic\" onclick=\"RDOM.Rcall(&quot;demo&quot;, this, [ &quot;HTML&quot; ], null)\"> span 1</span>"

The next example is more complex. This time, demo will be called with four arguments: an HTML representation for the <span> element; a CSS selector for the <span> element; and both an HTML representation and a CSS selector for the parent of the <span> element (in that order).

setAttribute(page,
           elt=css("span"),
           "onclick",
           'RDOM.Rcall("demo", [ this, this.parentNode ], [ "HTML", "CSS" ], null)')

Again, we can simulate a click and look at the value of the clickResult to see the arguments that were sent to demo.

click(page, elt=css("span"))
clickResult
  [[1]]
  An object of class "DOM_node_HTML"
  [1] "<span style=\"font-style: italic\" onclick=\"RDOM.Rcall(&quot;demo&quot;, [ this, this.parentNode ], [ &quot;HTML&quot;, &quot;CSS&quot; ], null)\"> span 1</span>"
  
  [[2]]
  An object of class "DOM_node_CSS"
  [1] "span"
  
  [[3]]
  An object of class "DOM_node_HTML"
  [1] "<p style=\"color: red\">Paragraph 2<span style=\"font-style: italic\" onclick=\"RDOM.Rcall(&quot;demo&quot;, [ this, this.parentNode ], [ &quot;HTML&quot;, &quot;CSS&quot; ], null)\"> span 1</span></p>"
  
  [[4]]
  An object of class "DOM_node_CSS"
  [1] "body > :nth-child(1)"

New features

In addition to tidying up the internal code and the external API of the 'DOM' package, the refactoring of the package allows for the addition of some new features.

The first addition is the ability to represent DOM nodes within R as "pointers". The following code adds a new paragraph to the web page and returns the result as a DOM_node_ptr object. This demonstrates the new nodePtr function, which allows us to specify that we want the return result of the call to appendChild to be a pointer to the DOM node that was added.

para4 <- appendChild(page,
                 child=htmlNode("<p>Paragraph 4</p>"),
                 response=nodePtr())
para4
  An object of class "DOM_node_ptr"
  [1] "0"

The next example shows that these pointer representations can be used to specify DOM nodes in subsequent modifications of the web page. In this case, a new <span> is being added, with the parent paragraph specified by a DOM_node_ptr.

appendChild(page,
          child=htmlNode('<span style="font-style: italic"> span 2</span>'),
          parent=para4)
  An object of class "DOM_node_HTML"
  [1] "<span style=\"font-style: italic\"> span 2</span>"

Another way to generate a pointer to a DOM node is with the new createElement function. This function creates a new DOM node that is not part of the web page. The following code creates a <div> element, without adding it to the web page, and returns a pointer to the new element.

container <- createElement(page, "div")

There is also a createElementNS function for creating elements with a specific namespace (e.g., SVG elements).

The next example demonstrates that it is now possible to create content "off screen", by working with these DOM nodes that are not part of the web page. In this case, we set the style on the <div> element, so that its contents will be green, and add a paragraph to the <div>, all without changing the web page.

setAttribute(page,
           elt=container,
           "style", "color: green")
appendChild(page,
          child=htmlNode("<p>Paragraph 5</p>"),
          parent=container)
  An object of class "DOM_node_HTML"
  [1] "<p>Paragraph 5</p>"

We can then add the complete <div> to the web page, including its paragraph content, as a final step.

appendChild(page,
          child=container)
  An object of class "DOM_node_HTML"
  [1] "<div style=\"color: green\"><p>Paragraph 5</p></div>"

Summary

In version 0.3, the 'DOM' package has undergone a major refactoring. This has changed the API for most public functions in the package (and has removed several functions). The benefits of the changes are a simpler and smaller API, plus it is easier to add new features to the package.

The new features in this version are the createElement function (and the createElementNS function), for creating DOM nodes that are not part of a web page, and the ability to work with "pointers" to DOM nodes (in addition to the existing explicit HTML/SVG or CSS selectors or Xpath expressions). These new features make it possible to work "off screen" to generate web page content, so that the browser does not have to redraw the web page after every single request from R to the browser.

Technical requirements

The examples and discussion in this document relate to version 0.3 of the 'DOM' package.

This report was generated within a Docker container (see Resources section below).

Resources


Creative Commons License
'DOM' version 0.3 by Paul Murrell is licensed under a Creative Commons Attribution 4.0 International License.