Skip to content

Commit 9186175

Browse files
committed
Fix JS evals not working in meta, use htmlwidgets TOJSON_FUNC attribute
* Keep old behavior of preserializing data as an optimization * Add json_verbatim to toJSON
1 parent eca1966 commit 9186175

12 files changed

Lines changed: 79 additions & 36 deletions

File tree

R/reactable.R

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -541,13 +541,10 @@ reactable <- function(
541541
stop("`meta` must be a named list")
542542
}
543543
# Omit empty lists and objects for consistency with htmltools tag behavior when supplying
544-
# empty attributes.
544+
# empty attributes. This was previously done implicitly in reactR::component(), but now done
545+
# explicitly here.
545546
if (length(meta) == 0) {
546547
meta <- NULL
547-
} else {
548-
# Use the same JSON serialization as reactable, not htmlwidgets (which differs very slightly)
549-
# for consistency, since meta can contain complex data types.
550-
meta <- toJSON(meta)
551548
}
552549
}
553550

@@ -718,11 +715,10 @@ reactable <- function(
718715
}
719716
}
720717

721-
# Override the htmlwidgets default JSON serialization options for data:
722-
#
723-
# * Serialize numbers with max precision
724-
# * Preserve numeric NA, NaN, Inf, and -Inf as strings
725-
# * Serialize both dates/datetimes as ISO 8601
718+
# Pre-compute / pre-serialize data to JSON to optimize for large datasets that can take seconds to serialize.
719+
# By pre-serializing, we only pay the serialization cost once at the start, rather than every time when
720+
# rendering or printing the widget. This was originally done as a necessity to change the JSON serializer,
721+
# but now kept as an intentional optimization.
726722
data <- toJSON(data)
727723

728724
# Create a unique key for the data. The key is used to fully reset state when
@@ -791,9 +787,23 @@ reactable <- function(
791787
# Temporary workaround for JS() not working in htmlwidgets 1.6.3
792788
class(component) <- c(class(component), "list")
793789

790+
# Override the htmlwidgets default JSON serialization options for meta and other attributes:
791+
#
792+
# * Serialize numbers with max precision (typically 15 digits depending on the installed jsonlite version).
793+
# * Preserve numeric NA, NaN, Inf, and -Inf as strings. String NAs are still serialized as `null`.
794+
# * Serialize both datetimes and dates as ISO 8601 in UTC timezone.
795+
#
796+
# Use TOJSON_FUNC attribute, rather than serializing meta ourselves, because htmlwidgets
797+
# will also process JS() evals. If meta is converted to JSON before reaching htmlwidgets,
798+
# those JS evals will be lost.
799+
#
800+
# https://www.htmlwidgets.org/develop_advanced.html
801+
markup <- reactR::reactMarkup(component)
802+
attr(markup, "TOJSON_FUNC") <- toJSON
803+
794804
htmlwidgets::createWidget(
795805
name = "reactable",
796-
reactR::reactMarkup(component),
806+
markup,
797807
width = width,
798808
height = height,
799809
sizingPolicy = htmlwidgets::sizingPolicy(
@@ -807,7 +817,7 @@ reactable <- function(
807817
elementId = elementId,
808818
preRenderHook = preRenderHook
809819
)
810-
}
820+
}
811821

812822
# Convert named list of column orders to { id, desc } definitions
813823
columnSortDefs <- function(defaultSorted) {

R/utils.R

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ toJSON <- function(
3232
UTC = TRUE,
3333
force = TRUE,
3434
auto_unbox = TRUE,
35-
null = "null"
35+
null = "null",
36+
# Match htmlwidgets behavior of not double-serializing JSON
37+
json_verbatim = TRUE
3638
)
3739
}
3840

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,63 @@
1-
{{^as_is}}$for(header-includes)$ $header-includes$ $endfor${{/as_is}}
1+
<!-- Last updated: pkgdown 2.1.3 -->
2+
{{^as_is}}$for(header-includes)$
3+
$header-includes$
4+
$endfor${{/as_is}}
25

36
<div class="row">
4-
<!-- Don't add toc if the article has no toc -->
7+
<!-- Allow content to be full-width if article has no toc -->
58
<main id="main" $if(toc)$class="col-md-9" $endif$>
69
<div class="page-header">
7-
{{#logo}}<img src="{{logo.src}}" class="logo" alt="" />{{/logo}}
10+
{{#logo}}<img src="{{logo.src}}" class="logo" alt="" >{{/logo}}
811
<h1>{{{pagetitle}}}</h1>
912
$if(subtitle)$
1013
<h3 data-toc-skip class="subtitle">$subtitle$</h3>
11-
$endif$ $for(author)$ $if(author.name)$
14+
$endif$
15+
$for(author)$
16+
$if(author.name)$
1217
<h4 data-toc-skip class="author">$author.name$</h4>
1318
$if(author.affiliation)$
1419
<address class="author_afil">
15-
$author.affiliation$<br />$endif$ $if(author.email)$
16-
<a class="author_email" href="mailto:#">$author.email$</a>
20+
$author.affiliation$<br>$endif$
21+
$if(author.email)$
22+
<a class="author_email" href="mailto:#">$author.email$</a>
1723
</address>
18-
$endif$ $else$
24+
$endif$
25+
$else$
1926
<h4 data-toc-skip class="author">$author$</h4>
20-
$endif$ $endfor$ $if(date)$
27+
$endif$
28+
$endfor$
29+
30+
$if(date)$
2131
<h4 data-toc-skip class="date">$date$</h4>
22-
$endif$ {{#source}}<small class="dont-index">{{{.}}}</small>{{/source}}
32+
$endif$
33+
34+
{{#source}}<small class="dont-index">{{{.}}}</small>{{/source}}
2335
<div class="d-none name"><code>{{filename}}</code></div>
2436
</div>
2537

26-
$for(include-before)$ $include-before$ $endfor$ $if(abstract)$
38+
$for(include-before)$
39+
$include-before$
40+
$endfor$
41+
42+
$if(abstract)$
2743
<div class="abstract">
28-
<p class="abstract">Abstract</p>
44+
<p class="abstract">{{#translate}}{{abstract}}{{/translate}}</p>
2945
$abstract$
3046
</div>
31-
$endif$ $body$
47+
$endif$
48+
49+
$body$
3250
</main>
33-
$if(toc)$
51+
{{#toc}}
3452
<aside class="col-md-3">
35-
<nav id="toc">
53+
<nav id="toc" aria-label="{{#translate}}{{toc}}{{/translate}}">
3654
<h2>{{#translate}}{{on_this_page}}{{/translate}}</h2>
3755
</nav>
3856
</aside>
39-
$endif$
57+
{{/toc}}
58+
4059
</div>
4160

42-
$for(include-after)$ $include-after$ $endfor$
61+
$for(include-after)$
62+
$include-after$
63+
$endfor$

srcjs/__tests__/server.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { renderToHTML, setInitialProps, renderToData, resetTestRenderer } from '
1010
describe('renderToHTML', () => {
1111
it('renders tables to HTML', () => {
1212
const inputString =
13-
'{"props":{"data":"{\\"Manufacturer\\":[\\"Acura\\",\\"Acura\\",\\"Audi\\"],\\"Model\\":[\\"Integra\\",\\"Legend\\",\\"90\\"],\\"Type\\":[\\"Small\\",\\"Midsize\\",\\"Compact\\"]}","columns":[{"id":"Manufacturer","name":"Manufacturer","type":"factor"},{"id":"Model","name":"Model","type":"factor"},{"id":"Type","name":"Type","type":"factor"}],"dataKey":"b540d0d929ae2ade6248d0cbb7f477fc"},"evals":[]}'
13+
'{"props":{"data":{"Manufacturer":["Acura","Acura","Audi"],"Model":["Integra","Legend","90"],"Type":["Small","Midsize","Compact"]},"columns":[{"id":"Manufacturer","name":"Manufacturer","type":"factor"},{"id":"Model","name":"Model","type":"factor"},{"id":"Type","name":"Type","type":"factor"}],"dataKey":"b540d0d929ae2ade6248d0cbb7f477fc"},"evals":[]}'
1414
const { html, css, ids } = renderToHTML(inputString)
1515
expect(html).toMatchSnapshot()
1616
expect(css).toEqual('')
@@ -19,7 +19,7 @@ describe('renderToHTML', () => {
1919

2020
it('renders tables to HTML with JS evals', () => {
2121
const inputString =
22-
'{"props":{"data":"{\\"Manufacturer\\":[\\"Acura\\",\\"Acura\\"],\\"Model\\":[\\"Integra\\",\\"Legend\\"]}","columns":[{"id":"Manufacturer","name":"Manufacturer","type":"factor"},{"id":"Model","name":"Model","type":"factor","cell":"cellInfo => cellInfo.value + \'__\'"}],"dataKey":"7951e598fecb5fa57c37a854a0917a7c"},"evals":["columns.1.cell"]}'
22+
'{"props":{"data":{"Manufacturer":["Acura","Acura"],"Model":["Integra","Legend"]},"columns":[{"id":"Manufacturer","name":"Manufacturer","type":"factor"},{"id":"Model","name":"Model","type":"factor","cell":"cellInfo => cellInfo.value + \'__\'"}],"dataKey":"7951e598fecb5fa57c37a854a0917a7c"},"evals":["columns.1.cell"]}'
2323
const { html, css, ids } = renderToHTML(inputString)
2424
expect(html).toContain('Integra__')
2525
expect(html).toContain('Legend__')
@@ -30,7 +30,7 @@ describe('renderToHTML', () => {
3030

3131
it('renders tables to HTML with theme styles', () => {
3232
const inputString =
33-
'{"props":{"data":"{\\"Manufacturer\\":[\\"Acura\\",\\"Acura\\"],\\"Model\\":[\\"Integra\\",\\"Legend\\"]}","columns":[{"id":"Manufacturer","name":"Manufacturer","type":"factor"},{"id":"Model","name":"Model","type":"factor"}],"theme":{"color":"red","cellPadding":"1rem"},"dataKey":"01768d44dd8153352ff1cfa908f38d72"},"evals":[]}'
33+
'{"props":{"data":{"Manufacturer":["Acura","Acura"],"Model":["Integra","Legend"]},"columns":[{"id":"Manufacturer","name":"Manufacturer","type":"factor"},{"id":"Model","name":"Model","type":"factor"}],"theme":{"color":"red","cellPadding":"1rem"},"dataKey":"01768d44dd8153352ff1cfa908f38d72"},"evals":[]}'
3434
const { html, css, ids } = renderToHTML(inputString)
3535
expect(html).toMatchSnapshot()
3636
expect(css).toEqual('.reactable-1galy0v{color:red;}.reactable-1rsvkai{padding:1rem;}')

srcjs/server.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ export function renderToHTML(inputJson) {
2222
const input = JSON.parse(inputJson)
2323

2424
const props = input.props
25-
// Table data comes through double-serialized, first with reactable's custom serialization
26-
// options, and then with the htmlwidgets default serialization.
27-
props.data = JSON.parse(props.data)
2825

2926
// Resolve strings marked as JavaScript literals to objects
3027
if (input.evals) {

tests/testthat/test-reactable.R

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1310,8 +1310,10 @@ test_that("meta", {
13101310
emptyList = list() # will be serialized as []
13111311
)
13121312
tbl <- reactable(data, meta = meta, rowStyle = JS("(rowInfo, state) => { console.log(state.meta) }"))
1313+
# meta must not be pre-serialized or the JS() func will be lost
1314+
expect_equal(getAttrib(tbl, "meta"), meta)
13131315
expected <- '{"number":30,"str":"str","df":{"y":[2]},"func":"value => value > 30","na":null,"naInteger":"NA","null":null,"inf":"Inf","date":"2019-01-02T03:22:15Z","array":[2,4,6],"arrayLength1":1,"list":[1],"emptyList":[]}'
1314-
expect_equal(as.character(getAttrib(tbl, "meta")), expected)
1316+
expect_equal(as.character(toJSON(getAttrib(tbl, "meta"))), expected)
13151317
})
13161318

13171319
test_that("elementId", {

tests/testthat/test-utils.R

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
library(htmltools)
22

3+
test_that("toJSON", {
4+
# Should not double-serialize JSON to match htmlwidgets behavior
5+
x <- list(x = 1, json = toJSON(list(y = "b")))
6+
expect_equal(as.character(toJSON(x)), '{"x":1,"json":{"y":"b"}}')
7+
})
8+
39
test_that("mergeLists", {
410
a <- list(a = 1, b = "b", c = 3)
511
b <- list(a = 2, c = 4, d = "d")

vignettes/cran-packages/cran-packages.Rmd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
title: "CRAN Packages"
33
output: html_document
4+
toc: false
45
resource_files:
56
- '.'
67
---

vignettes/nba-box-score/nba-box-score.Rmd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
title: NBA Box Score
33
output: html_document
4+
toc: false
45
resource_files:
56
- '.'
67
---

vignettes/popular-movies/popular-movies.Rmd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
title: "Popular Movies"
33
output: html_document
4+
toc: false
45
resource_files:
56
- '.'
67
---

0 commit comments

Comments
 (0)