diff --git a/.Rbuildignore b/.Rbuildignore index 3ccfcf0..94fc918 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,3 +1,4 @@ +^node_modules$ ^.*\.Rproj$ ^\.Rproj\.user$ ^\build @@ -5,3 +6,7 @@ ^cran-comments\.md$ ^docs$ ^\.travis\.yml$ +^\assets +^karma\.conf\.js$ +^package\.json$ +^yarn\.lock$ diff --git a/.gitignore b/.gitignore index c833a2c..a422f90 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ .RData .Ruserdata inst/doc +node_modules +reactR.Rcheck +reactR_*.tar.gz \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index d326275..092d345 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,15 @@ -# R for travis: see documentation at https://docs.travis-ci.com/user/languages/r - -language: R -sudo: required -cache: packages - -before_install: - - sudo apt-get install -y libv8-dev +matrix: + include: + # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r + - language: r + r: + - release + sudo: required + cache: packages + before_install: + - sudo apt-get install -y libv8-dev + - language: node_js + node_js: + - "11.4.0" + addons: + - chrome: stable \ No newline at end of file diff --git a/DESCRIPTION b/DESCRIPTION index 4bf8d99..98ae489 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: reactR Type: Package Title: React Helpers -Version: 0.2.1 -Date: 2018-10-23 +Version: 0.3.0 +Date: 2019-01-11 Authors@R: c( person( "Facebook", "Inc" @@ -14,16 +14,23 @@ Authors@R: c( , role = c("aut", "cre") , comment = "R interface" , email = "kent.russell@timelyportfolio.com" + ), + person( + "Alan", "Dipert" + , role = c("aut") + , comment = "R interface" + , email = "alan@rstudio.com" ) ) Maintainer: Kent Russell -Description: Make it easy to use 'React' in R with helper - dependency functions, embedded 'Babel' 'transpiler', +Description: Make it easy to use 'React' in R with 'htmlwidget' scaffolds, + helper dependency functions, an embedded 'Babel' 'transpiler', and examples. -URL: https://github.com/timelyportfolio/reactR -BugReports: https://github.com/timelyportfolio/reactR/issues +URL: https://github.com/react-R/reactR +BugReports: https://github.com/react-R/reactR/issues License: MIT + file LICENSE LazyData: TRUE +Encoding: UTF-8 Imports: htmltools Suggests: @@ -32,5 +39,5 @@ Suggests: shiny, V8, knitr -RoxygenNote: 6.0.1 +RoxygenNote: 6.1.1 VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index d4330e3..14a93f4 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,15 @@ # Generated by roxygen2: do not edit by hand +S3method("$",react_component_builder) +S3method("$<-",react_component_builder) +S3method("[[",react_component_builder) +S3method("[[<-",react_component_builder) +export(React) export(babel_transform) +export(component) export(html_dependency_corejs) export(html_dependency_react) +export(html_dependency_reacttools) +export(reactMarkup) +export(scaffoldReactWidget) importFrom(htmltools,htmlDependency) diff --git a/R/dependencies.R b/R/dependencies.R index f9b8f69..5dc4870 100644 --- a/R/dependencies.R +++ b/R/dependencies.R @@ -70,3 +70,18 @@ html_dependency_corejs <- function() { script = "shim.min.js" ) } + +#' Adds window.reactR.exposeComponents and window.reactR.hydrate +#' +#' @return \code{\link[htmltools]{htmlDependency}} +#' @importFrom htmltools htmlDependency +#' @export +html_dependency_reacttools <- function(){ + htmltools::htmlDependency( + name = "reactwidget", + src = "www/react-tools", + version = "1.0.0", + package = "reactR", + script = c("react-tools.js") + ) +} diff --git a/R/meta.R b/R/meta.R index 8ceac75..b943a1e 100644 --- a/R/meta.R +++ b/R/meta.R @@ -1,3 +1,3 @@ #'@keywords internal -react_version <- function(){'16.6.0'} +react_version <- function(){'16.7.0'} babel_version <- function(){'6.26.0'} \ No newline at end of file diff --git a/R/reacttools.R b/R/reacttools.R new file mode 100644 index 0000000..915566c --- /dev/null +++ b/R/reacttools.R @@ -0,0 +1,94 @@ +isUpper <- function(s) { + grepl("^[[:upper:]]+$", s) +} + +#' Create a React component +#' +#' @param name Name of the React component, which must start with an upper-case +#' character. +#' @param varArgs Attributes and children of the element to pass along to +#' \code{\link[htmltools]{tag}} as \code{varArgs}. +#' +#' @return An htmltools \code{\link[htmltools]{tag}} object +#' @export +#' +#' @examples +#' component("ParentComponent", +#' list( +#' x = 1, +#' y = 2, +#' component("ChildComponent"), +#' component("OtherChildComponent") +#' ) +#' ) +component <- function(name, varArgs = list()) { + if (length(name) == 0 || !isUpper(substring(name, 1, 1))) { + stop("Component name must be specified and start with an upper case character") + } + component <- htmltools::tag(name, varArgs) + structure(component, class = c("reactR_component", oldClass(component))) +} + +#' React component builder. +#' +#' \code{React} is a syntactically-convenient way to create instances of React +#' components that can be sent to the browser for display. It is a list for +#' which \link[=InternalMethods]{extract methods} are defined, allowing +#' object creation syntax like \code{React$MyComponent(x = 1)} where +#' \code{MyComponent} is a React component you have exposed to Shiny in +#' JavaScript. +#' +#' Internally, the \code{\link{component}} function is used to create the +#' component instance. +#' +#' @examples +#' # Create an instance of ParentComponent with two children, +#' # ChildComponent and OtherChildComponent. +#' React$ParentComponent( +#' x = 1, +#' y = 2, +#' React$ChildComponent(), +#' React$OtherChildComponent() +#' ) +#' @export +React <- structure( + list(), + class = "react_component_builder" +) + +#' @export +`$.react_component_builder` <- function(x, name) { + function(...) { + component(name, list(...)) + } +} + +#' @export +`[[.react_component_builder` <- `$.react_component_builder` + +#' @export +`$<-.react_component_builder` <- function(x, name, value) { + stop("Assigning to a component constructor is not allowed") +} + +#' @export +`[[<-.react_component_builder` <- `$<-.react_component_builder` + +#' Prepare data that represents a single-element character vector, a React +#' component, or an htmltools tag for sending to the client. +#' +#' Tag lists as returned by \code{\link[htmltools]{tagList}} are not currently +#' supported. +#' +#' @param tag character vector or React component or +#' \code{\link[htmltools]{tag}} +#' +#' @return A reactR markup object suitable for being passed to +#' \code{\link[htmlwidgets]{createWidget}} as widget instance data. +#' @export +reactMarkup <- function(tag) { + stopifnot(inherits(tag, "shiny.tag") + || (is.character(tag) && length(tag) == 1)) + list(tag = tag, class = "reactR_markup") +} + diff --git a/R/scaffold.R b/R/scaffold.R new file mode 100644 index 0000000..f0b1383 --- /dev/null +++ b/R/scaffold.R @@ -0,0 +1,157 @@ +#' Create implementation scaffolding for a React.js-based HTML widget +#' +#' Add the minimal code required to implement a React.js-based HTML widget to an +#' R package. +#' +#' @param name Name of widget +#' @param npmPkg Optional \href{https://npmjs.com/}{NPM} package upon which this +#' widget is based, a named list with two elements: \code{name} and +#' \href{https://docs.npmjs.com/files/package.json#dependencies}{version}. If +#' you specify this parameter the package will be added to the +#' \code{dependency} section of the generated \code{package.json}. +#' @param edit Automatically open the widget's JavaScript source file after +#' creating the scaffolding. +#' +#' @note This function must be executed from the root directory of the package +#' you wish to add the widget to. +#' +#' @export +scaffoldReactWidget <- function(name, npmPkg = NULL, edit = interactive()){ + if (!file.exists('DESCRIPTION')){ + stop( + "You need to create a package to house your widget first!", + call. = F + ) + } + if (!file.exists('inst')){ + dir.create('inst') + } + package <- read.dcf('DESCRIPTION')[[1,"Package"]] + addWidgetConstructor(name, package, edit) + addWidgetYAML(name, edit) + addPackageJSON(toDepJSON(npmPkg)) + addWebpackConfig(name) + addWidgetJS(name, edit) + addExampleApp(name) + message("To install dependencies from npm run: yarn install") + message("To build JavaScript run: yarn run webpack --mode=development") +} + +toDepJSON <- function(npmPkg) { + if (is.null(npmPkg)) { + "" + } else { + sprintf('"%s": "%s"', npmPkg$name, npmPkg$version) + } +} + +slurp <- function(file) { + paste(readLines( + system.file(file, package = 'reactR') + ), collapse = "\n") +} + +# Perform a series of pattern replacements on str. +# Example: renderTemplate("foo ${x} bar ${y} baz ${x}", list(x = 1, y = 2)) +# Produces: "foo 1 bar 2 baz 1" +renderTemplate <- function(str, substitutions) { + Reduce(function(str, name) { + gsub(paste0("\\$\\{", name, "\\}"), substitutions[[name]], str) + }, names(substitutions), str) +} + +capName = function(name){ + paste0(toupper(substring(name, 1, 1)), substring(name, 2)) +} + +addWidgetConstructor <- function(name, package, edit){ + tpl <- slurp('templates/widget_r.txt') + if (!file.exists(file_ <- sprintf("R/%s.R", name))){ + cat( + renderTemplate(tpl, list(name = name, package = package, capName = capName(name))), + file = file_ + ) + message('Created boilerplate for widget constructor ', file_) + } else { + message(file_, " already exists") + } + if (edit) fileEdit(file_) +} + +addWidgetYAML <- function(name, edit){ + tpl <- "# (uncomment to add a dependency) +# dependencies: +# - name: +# version: +# src: +# script: +# stylesheet: +" + if (!file.exists('inst/htmlwidgets')){ + dir.create('inst/htmlwidgets') + } + if (!file.exists(file_ <- sprintf('inst/htmlwidgets/%s.yaml', name))){ + cat(tpl, file = file_) + message('Created boilerplate for widget dependencies at ', + sprintf('inst/htmlwidgets/%s.yaml', name) + ) + } else { + message(file_, " already exists") + } + if (edit) fileEdit(file_) +} + +addPackageJSON <- function(npmPkg) { + tpl <- renderTemplate(slurp('templates/widget_package.json.txt'), list(npmPkg = npmPkg)) + if (!file.exists('package.json')) { + cat(tpl, file = 'package.json') + message('Created package.json') + } else { + message("package.json already exists") + } +} + +addWebpackConfig <- function(name) { + tpl <- renderTemplate(slurp('templates/widget_webpack.config.js.txt'), list(name = name)) + if (!file.exists('webpack.config.js')) { + cat(tpl, file = 'webpack.config.js') + message('Created webpack.config.js') + } else { + message("webpack.config.js already exists") + } +} + +addWidgetJS <- function(name, edit){ + tpl <- paste(readLines( + system.file('templates/widget_js.txt', package = 'reactR') + ), collapse = "\n") + if (!file.exists('srcjs')){ + dir.create('srcjs') + } + if (!file.exists(file_ <- sprintf('srcjs/%s.js', name))){ + cat(renderTemplate(tpl, list(name = name)), file = file_) + message('Created boilerplate for widget javascript bindings at ', + sprintf('srcjs/%s.js', name) + ) + } else { + message(file_, " already exists") + } + if (edit) fileEdit(file_) +} + +addExampleApp <- function(name) { + tpl <- renderTemplate(slurp('templates/widget_app.R.txt'), list(name = name, capName = capName(name))) + if (!file.exists('app.R')) { + cat(tpl, file = 'app.R') + message('Created example app.R') + } else { + message("app.R already exists") + } +} + +# invoke file.edit in a way that will bind to the RStudio editor +# when running inside RStudio +fileEdit <- function(file) { + fileEditFunc <- eval(parse(text = "file.edit"), envir = globalenv()) + fileEditFunc(file) +} diff --git a/README.md b/README.md index 147f035..443d79d 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,33 @@ [![CRAN_Status_Badge](http://www.r-pkg.org/badges/version/reactR)](https://cran.r-project.org/package=reactR) -[![Travis-CI Build Status](https://travis-ci.org/timelyportfolio/reactR.svg?branch=master)](https://travis-ci.org/timelyportfolio/reactR) +[![Travis-CI Build Status](https://travis-ci.org/react-R/reactR.svg?branch=master)](https://travis-ci.org/react-R/reactR) + +react-R logo # reactR -A set of convenience function with local dependencies for using [`React`](https://facebook.github.io/react) in `R`. This is modeled after the `html_dependency_*` functions from RStudio's [`rmarkdown`](https://github.com/rstudio/rmarkdown) package. +`reactR` provides a set of convenience functions for using [`React`](https://facebook.github.io/react) in `R` with `htmlwidget` constructor templates and local JavaScript dependencies. The `React` ecosystem is rich with components that can enhance `R` web and Shiny apps. `scaffoldReactWidget()` helps build `htmlwidgets` to integrate these `React` components as `R` `htmlwidgets`. The local dependency functions are modeled after the `html_dependency_*` functions from RStudio's [`rmarkdown`](https://github.com/rstudio/rmarkdown) package. ## Installation -You can install reactR from github with: +You can install reactR from CRAN with `install.packages("reactR")`. For the development version, please use `devtools` as shown below. ```R # install.packages("devtools") -devtools::install_github("timelyportfolio/reactR") +devtools::install_github("react-R/reactR") ``` -## Example +## Creating htmlwidgets with React Components + +To wrap a `React` component as an `htmlwidget`, please see the tutorial [htmlwidgets with reactR](https://react-r.github.io/reactR/articles/intro_htmlwidgets.html). Also, there are a variety of examples in the [react-R Github organization](https://github.com/react-R). + +## Examples ```R library(reactR) library(htmltools) browsable(tagList( + tags$div(id = "app"), tags$script( " ReactDOM.render( @@ -29,7 +36,7 @@ browsable(tagList( null, 'Powered by React' ), - document.body + document.getElementById('app') ) " ), @@ -39,7 +46,7 @@ browsable(tagList( )) ``` -`reactR` also uses `V8` if available to transform `JSX` and `ES2015` code. +`reactR` also uses the `V8` package if available to transform `JSX` and `ES2015` code. ```R library(reactR) @@ -47,8 +54,9 @@ library(htmltools) browsable( tagList( + tags$div(id = "app"), tags$script( - babel_transform('ReactDOM.render(

Powered By React/JSX

,document.body)') + babel_transform('ReactDOM.render(

Powered By React/JSX

,document.getElementById("app"))') ), # add core-js shim first for React in RStudio Viewer html_dependency_corejs(), @@ -59,4 +67,4 @@ browsable( ## Contributing and Code of Conduct -I welcome contributors. Help make this package great. Please note that this project is released with a [Contributor Code of Conduct](CONDUCT.md). By participating in this project you agree to abide by its terms. +We welcome contributors and would love your participation. Please note that this project is released with a [Contributor Code of Conduct](CONDUCT.md). By participating in this project you agree to abide by the terms. diff --git a/assets/logos/reactR-logo-inkscape.svg b/assets/logos/reactR-logo-inkscape.svg new file mode 100644 index 0000000..fcf4673 --- /dev/null +++ b/assets/logos/reactR-logo-inkscape.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/assets/logos/reactR-logo.png b/assets/logos/reactR-logo.png new file mode 100644 index 0000000..d2e8610 Binary files /dev/null and b/assets/logos/reactR-logo.png differ diff --git a/assets/logos/reactR-logo.svg b/assets/logos/reactR-logo.svg new file mode 100644 index 0000000..d509c58 --- /dev/null +++ b/assets/logos/reactR-logo.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/docs/LICENSE.html b/docs/LICENSE.html index e947260..9fb0c0e 100644 --- a/docs/LICENSE.html +++ b/docs/LICENSE.html @@ -67,6 +67,9 @@