Literate JavaScript Programming

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().

---
output: 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.

const x = 10
console.log("multiplying x times 20...")
x * 20

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.

true && false

let x = 12
x = x * 4 - 6

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.

x

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.

const x = 42
const x = 64

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.)

globalThis.x = 12
x = x * 4 - 6

If you try to access x in another chunk, you’ll get the global x

x

but you can still create local variables that mask global variables.

const x = 12
x

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.

document.getElementById('out-hello-world').innerHTML = '<p><em>Hello, world!</em></p>'

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.

<p style="color: #338D70">This text is green, right?</p>

This text is green, right?