js4shiny includes several components that enable the
use of R Markdown for literate programming with JavaScript. The first of
these is the R Markdown HTML format,
js4shiny::html_document_js()
.
Alternatively, you can override the js
knitr chunk
engine of any HTML-based R Markdown format. This function also registers
json and html knitr engines.
js4shiny::register_knitr_js_engine()
Introducing html_document_js
The html_document_js
renders R Markdown to HTML and uses
a new JavaScript knitr engine that runs each JavaScript chunk in the
browser and writes the output of any console.log()
, or the
return value of the chunk, into the HTML document, interactively in the
browser.
In other words, it renders JavaScript chunks that kind of like R chunks. Here’s a standard R chunk.
x <- 10
message("multiplying x times 20...")
#> multiplying x times 20...
x * 20
#> [1] 200
And here’s a JavaScript chunk.
When the document is viewed in a browser, each JavaScript chunk is
evaluated in its own block scope and any console.log()
statements and the return value of the block are automatically printed
to the chunk’s output <div>
, unless that value is
undefined
.
The last statement in a chunk doesn’t need console.log()
to be output. This makes it easier to show the results of code without
requiring numerous console.log()
statements.
There are a few differences between R chunks and JavaScript chunks. The first is that JavaScript results are always grouped together in the output code block, whereas output from R chunks is printed after the expression causing the output.
The second difference is that R chunks build upon previous chunks; each R chunk is executed in the same environment and the results from each chunk are available to subsequent chunks. The JavaScript chunks, on the other hand, are each executed in a separate block scope, so the result of a chunk isn’t necessarily available for later code. I’ll explain and provide methods for working around this limitation in the next section.
Scoping JavaScript Chunks
Because each chunk is block-scoped, variables created in one block
may not be available to other chunks. Notice that if I try to access
x
from the previous JavaScript chunk, where x
is eventually assigned 42
, I get an error that
x
is not defined.
Because variables in JavaScript need to be declared with
let
, const
, or var
, this may be
preferred to executing everything in a shared environment. For example,
in the first JavaScript chunk in this vignette, I declared
const x = 10
, but in a later chunk I declared
const x = 12
. If all chunks were evaluated to build upon
previous chunks, then you would frequently run into “identifier already
declared” errors.
When you want a variable to exist beyond the scope of the current
block, two methods are available to create global variables. First, you
can assign your variable as a property of globalThis, a recent
addition to JavaScript that’s available in about 90% of browsers. (You
could also assign to window
instead of
globalThis
.)
If you try to access x
in another chunk, you’ll get the
global x
but you can still create local variables that mask global variables.
Alternatively, the entire chunk can be evaluated globally by
temporarily disabling the console redirect. Add
js_redirect = FALSE
to the chunk that you would like to be
evaluated in the global scope. These chunks use the standard
js
knitr engine, that simply embeds the js
code in a <script>
tag in your output. With this
option, console.log()
output will not appear in the
document, but logged statements will still be available in the
browser’s developer tools console. Helpfully, though, variables created
in these chunks are assigned in the global scope are thereafter
available to all chunks.
```{js, js_redirect = FALSE}
let globalVariable = 'this is a global variable'
console.log(globalVariable) // goes to browser console
```
```{js}
console.log(globalVariable) // outputs in document
```
Using the Output <div>
The live JavaScript chunks are written in to the document as a
<div>
and <script>
pair. For a
chunk like the following
```{js hello-world}
document.getElementById('out-simple-redirect').innerHTML = 'Hello, world!'
```
the following HTML is included in the output.
<div id="out-hello-world">
<pre><!-- output statements will appear here --></pre>
</div>
<script type="text/javascript">
// the javascript from the current chunk
</script>
This means that if your JavaScript chunk needs a dedicated element on
the page to use for writing the output, you can look for the element
with id out-<chunk-name-here>
(non-alphanumeric
characters in the chunk name are replaced with _
). This
element exists before the JavaScript is called — so you can
always know that it’s available for use — but after the static code
chunk is printed. This is particularly
useful if your JavaScript chunks are demonstrating d3.js visualizations.
Interactive Documents
There are a lot of interesting things you can do when you blend your JavaScript and HTML together. For instance, you’ve clicked the button below zero times. Go ahead and click it!
const btn = document.getElementById('click-me')
btn.addEventListener('click', function() {
const val = ++btn.value
btn.value = val
if (val > 9) console.log("Okay, that's enough!")
const text = document.getElementById('n_btn_clicks')
text.textContent = val === 1 ? '1 time' : `${val} times`
const textCTA = document.getElementById('click-again')
if (val === 1) {
textCTA.textContent = ' again.'
} else if (val >= 10) {
textCTA.textContent = ` again. Okay, that's enough, thank you!`
}
})
Other console
methods
You can also use other JavaScript console
methods, like
console.table()
.
const p1 = {first: 'Colin', last: 'Fay', country: 'France', twitter: '@_ColinFay'}
const p2 = {first: 'Garrick', last: 'Aden-Buie', country: 'USA', twitter: '@grrrck'}
console.table(p1)
console.table(p2)
JSON Chunks
To visualize or use JSON-formatted data in your document,
js4shiny also provides a json
knitr
engine. This next chunk is a JSON chunk named people
that
renders as an interactive list.
```{json people}
[
{
"first":"Colin",
"last":"Fay",
"country":"France",
"twitter":"@_ColinFay"
},
{
"first":"Garrick",
"last":"Aden-Buie",
"country":"USA",
"twitter":"@grrrck"
}
]
```
And the data from the JSON becomes a global variable named
data_<chunk_name>
, or in this case
data_people
.
data_people.forEach(function ({first, last, twitter}) {
console.log(`- ${first} ${last} (${twitter})`)
})
HTML Chunks
Finally, js4shiny::html_document_js
also provides an
HTML knitr engine. This is useful when you want the code of the HTML
itself to appear in the document, in addition to the HTML output.
This text is green, right?