by Paul Murrell http://orcid.org/0000-0002-3224-8858
Thursday 20 October 2016
'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.
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.
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>"
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("demo", this, [ "HTML" ], 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("demo", [ this, this.parentNode ], [ "HTML", "CSS" ], 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("demo", [ this, this.parentNode ], [ "HTML", "CSS" ], null)\"> span 1</span></p>" [[4]] An object of class "DOM_node_CSS" [1] "body > :nth-child(1)"
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>"
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.
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).
'DOM' version 0.3
by Paul
Murrell is licensed under a Creative
Commons Attribution 4.0 International License.