diff --git a/.Rbuildignore b/.Rbuildignore index 0b98076..59f2ef8 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -4,16 +4,16 @@ ^pkgdown$ ^assets$ ^.*\.Rproj$ -^\.Rproj\.user$ -^\build ^CONDUCT\.md$ -^cran-comments\.md$ -^docs$ +^README\.Rmd$ +^\.Rproj\.user$ ^\.travis\.yml$ -^\assets +^build$ +^docs$ +^cran-comments\.md$ ^karma\.conf\.js$ +^logo.svg$ ^package\.json$ +^webpack\.config\.js$ ^yarn\.lock$ -^README\.Rmd$ -logo.svg -webpack.config.js +^logo\.svg$ diff --git a/.gitignore b/.gitignore index 63e58fe..569985b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +Meta +doc .Rproj.user .Rhistory .RData @@ -7,3 +9,4 @@ node_modules reactR.Rcheck reactR_*.tar.gz *.swp +.DS_Store diff --git a/DESCRIPTION b/DESCRIPTION index 2672e03..9417f93 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: reactR Type: Package Title: React Helpers -Version: 0.3.1 -Date: 2019-01-26 +Version: 0.4.0 +Date: 2019-04-10 Authors@R: c( person( "Facebook", "Inc" @@ -39,6 +39,7 @@ Suggests: shiny, V8, knitr, - usethis + usethis, + jsonlite RoxygenNote: 6.1.1 VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 14a93f4..a996be6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,9 +7,11 @@ S3method("[[<-",react_component_builder) export(React) export(babel_transform) export(component) +export(createReactShinyInput) export(html_dependency_corejs) export(html_dependency_react) export(html_dependency_reacttools) export(reactMarkup) +export(scaffoldReactShinyInput) export(scaffoldReactWidget) importFrom(htmltools,htmlDependency) diff --git a/R/reacttools.R b/R/reacttools.R index b75a94c..b6dfbaa 100644 --- a/R/reacttools.R +++ b/R/reacttools.R @@ -1,3 +1,9 @@ +# A robust name string is a valid +# - CSS class +# - JavaScript variable name +# - R variable name +robustName <- "^[[:alpha:]_][[:alnum:]_]*$" + isUpper <- function(s) { grepl("^[[:upper:]]+$", s) } @@ -92,3 +98,57 @@ reactMarkup <- function(tag) { list(tag = tag, class = "reactR_markup") } +#' Create a React-based input +#' +#' @param inputId The \code{input} slot that will be used to access the value. +#' @param class Space-delimited list of CSS class names that should identify +#' this input type in the browser. +#' @param dependencies HTML dependencies to include in addition to those +#' supporting React. Must contain at least one dependency, that of the input's +#' implementation. +#' @param default Initial value. +#' @param configuration Static configuration data. +#' @param container Function to generate an HTML element to contain the input. +#' +#' @return Shiny input suitable for inclusion in a UI. +#' @export +#' +#' @examples +#' myInput <- function(inputId, default = "") { +#' # The value of createReactShinyInput should be returned from input constructor functions. +#' createReactShinyInput( +#' inputId, +#' "myinput", +#' # At least one htmlDependency must be provided -- the JavaScript implementation of the input. +#' htmlDependency( +#' name = "my-input", +#' version = "1.0.0", +#' src = "www/mypackage/myinput", +#' package = "mypackage", +#' script = "myinput.js" +#' ), +#' default +#' ) +#' } +createReactShinyInput <- function(inputId, + class, + dependencies, + default = NULL, + configuration = list(), + container = htmltools::tags$div) { + if(length(dependencies) < 1) stop("Must include at least one HTML dependency.") + value <- shiny::restoreInput(id = inputId, default = default) + htmltools::tagList( + html_dependency_corejs(), + html_dependency_react(), + html_dependency_reacttools(), + container(id = inputId, class = class), + htmltools::tags$script(id = sprintf("%s_value", inputId), + type = "application/json", + jsonlite::toJSON(value, auto_unbox = TRUE)), + htmltools::tags$script(id = sprintf("%s_configuration", inputId), + type = "application/json", + jsonlite::toJSON(configuration, auto_unbox = TRUE)), + dependencies + ) +} diff --git a/R/scaffold.R b/R/scaffold.R deleted file mode 100644 index 1a98bcf..0000000 --- a/R/scaffold.R +++ /dev/null @@ -1,163 +0,0 @@ -#' 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 npmPkgs Optional \href{https://npmjs.com/}{NPM} packages upon which -#' this widget is based which will be used to populate \code{package.json}. -#' Should be a named list of names to -#' \href{https://docs.npmjs.com/files/package.json#dependencies}{versions}. -#' @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, npmPkgs = 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(npmPkgs)) - addWebpackConfig(name) - addWidgetJS(name, edit) - addExampleApp(name) - - usethis::use_build_ignore(c("node_modules", "srcjs")) - usethis::use_git_ignore(c("node_modules")) - lapply(c("htmltools", "htmlwidgets", "reactR"), usethis::use_package) - - message("To install dependencies from npm run: yarn install") - message("To build JavaScript run: yarn run webpack --mode=development") -} - -toDepJSON <- function(npmPkgs) { - if (is.null(npmPkgs)) { - "" - } else if (!length(names(npmPkgs))) { - stop("Must specify npm package names in the names attributes of npmPkgs") - } else { - paste0(sprintf('"%s": "%s"', names(npmPkgs), npmPkgs), collapse = ",\n") - } -} - -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(npmPkgs) { - tpl <- renderTemplate(slurp('templates/widget_package.json.txt'), list(npmPkgs = npmPkgs)) - 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/R/scaffold_input.R b/R/scaffold_input.R new file mode 100644 index 0000000..fc9df6a --- /dev/null +++ b/R/scaffold_input.R @@ -0,0 +1,78 @@ +#' Create implementation scaffolding for a React.js-based Shiny input. +#' +#' Add the minimal code required to implement a React.js-based Shiny input to an +#' R package. +#' +#' @param name Name of input +#' @param npmPkgs Optional \href{https://npmjs.com/}{NPM} packages upon which +#' this input is based which will be used to populate \code{package.json}. +#' Should be a named list of names to +#' \href{https://docs.npmjs.com/files/package.json#dependencies}{versions}. +#' @param edit Automatically open the input's source files after creating the +#' scaffolding. +#' +#' @note This function must be executed from the root directory of the package +#' you wish to add the input to. +#' +#' @export +scaffoldReactShinyInput <- function(name, npmPkgs = NULL, edit = interactive()) { + assertNameValid(name) + package <- getPackage() + + file <- renderFile( + sprintf("R/%s.R", name), + "templates/input_r.txt", + "boilerplate for input constructor", + list( + name = name, + capName = capitalize(name), + package = package + ) + ) + if (edit) fileEdit(file) + + renderFile( + 'package.json', + 'templates/package.json.txt', + 'project metadata', + list(npmPkgs = toDepJSON(npmPkgs)) + ) + + renderFile( + 'webpack.config.js', + 'templates/webpack.config.js.txt', + 'webpack configuration', + list( + name = name, + outputPath = sprintf("inst/www/%s/%s", package, name) + ) + ) + + renderFile( + sprintf('srcjs/%s.jsx', name), + 'templates/input_js.txt', + 'JavaScript implementation', + list( + name = name, + package = package + ) + ) + + renderFile( + 'app.R', + 'templates/input_app.R.txt', + 'example app', + list( + name = name, + package = package + ) + ) + + usethis::use_build_ignore(c("node_modules", "srcjs", "app.R", "package.json", "webpack.config.js", "yarn.lock")) + usethis::use_git_ignore(c("node_modules")) + lapply(c("htmltools", "shiny", "reactR"), usethis::use_package) + + message("To install dependencies from npm run: yarn install") + message("To build JavaScript run: yarn run webpack --mode=development") +} + diff --git a/R/scaffold_utils.R b/R/scaffold_utils.R new file mode 100644 index 0000000..3cfc628 --- /dev/null +++ b/R/scaffold_utils.R @@ -0,0 +1,63 @@ +slurp <- function(file) { + paste(readLines( + system.file(file, package = 'reactR') + ), collapse = "\n") +} + +# 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) +} + +# 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) +} + +capitalize <- function(s) { + gsub("^(.)", perl = TRUE, replacement = '\\U\\1', s) +} + +toDepJSON <- function(npmPkgs) { + if (is.null(npmPkgs)) { + "" + } else if (!length(names(npmPkgs))) { + stop("Must specify npm package names in the names attributes of npmPkgs") + } else { + paste0(sprintf('"%s": "%s"', names(npmPkgs), npmPkgs), collapse = ",\n") + } +} + +# Wraps renderTemplate for convenient use from scaffold functions. +renderFile <- function(outputFile, templateFile, description = '', substitutions = list()) { + if (!file.exists(outputFile)) { + dir.create(dirname(outputFile), recursive = TRUE, showWarnings = FALSE) + cat(renderTemplate(slurp(templateFile), substitutions), file = outputFile) + message("Created ", description, " ", outputFile) + } else { + message(outputFile, " already exists") + } + outputFile +} + +getPackage <- function() { + if (!file.exists('DESCRIPTION')) { + stop("The current directory doesn't contain a package. You're either in the wrong directory, or need to create a package to house your widget.", call. = FALSE) + } + read.dcf('DESCRIPTION')[[1,"Package"]] +} + +# Constraining names prevents the user from encountering obscure CSS problems +# and JavaScript errors after scaffolding. +assertNameValid <- function(name) { + if (!grepl(robustName, name)) { + msg <- sprintf("Name '%s' is invalid, names must begin with an alphabetic character and must contain only alphabetic and numeric characters", name) + stop(msg, call. = FALSE) + } +} diff --git a/R/scaffold_widget.R b/R/scaffold_widget.R new file mode 100644 index 0000000..4240f1b --- /dev/null +++ b/R/scaffold_widget.R @@ -0,0 +1,101 @@ +#' 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 npmPkgs Optional \href{https://npmjs.com/}{NPM} packages upon which +#' this widget is based which will be used to populate \code{package.json}. +#' Should be a named list of names to +#' \href{https://docs.npmjs.com/files/package.json#dependencies}{versions}. +#' @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, npmPkgs = NULL, edit = interactive()){ + assertNameValid(name) + package <- getPackage() + + addWidgetConstructor(name, package, edit) + addWidgetYAML(name, edit) + addPackageJSON(toDepJSON(npmPkgs)) + addWebpackConfig(name) + addWidgetJS(name, edit) + addExampleApp(name) + + usethis::use_build_ignore(c("node_modules", "srcjs", "app.R", "package.json", "webpack.config.js", "yarn.lock")) + usethis::use_git_ignore(c("node_modules")) + lapply(c("htmltools", "htmlwidgets", "reactR"), usethis::use_package) + + message("To install dependencies from npm run: yarn install") + message("To build JavaScript run: yarn run webpack --mode=development") +} + +addWidgetConstructor <- function(name, package, edit){ + file <- renderFile( + sprintf("R/%s.R", name), + "templates/widget_r.txt", + "boilerplate for widget constructor", + list( + name = name, + package = package, + capName = capitalize(name) + ) + ) + if (edit) fileEdit(file) +} + +addWidgetYAML <- function(name, edit){ + file <- renderFile( + sprintf('inst/htmlwidgets/%s.yaml', name), + "templates/widget_yaml.txt", + "boilerplate for widget dependencies" + ) + if (edit) fileEdit(file) +} + +addPackageJSON <- function(npmPkgs) { + renderFile( + 'package.json', + 'templates/package.json.txt', + 'project metadata', + list(npmPkgs = npmPkgs) + ) +} + +addWebpackConfig <- function(name) { + renderFile( + 'webpack.config.js', + 'templates/webpack.config.js.txt', + 'webpack configuration', + list( + name = name, + outputPath = 'inst/htmlwidgets' + ) + ) +} + +addWidgetJS <- function(name, edit){ + file <- renderFile( + sprintf('srcjs/%s.jsx', name), + 'templates/widget_js.txt', + 'boilerplate for widget JavaScript bindings', + list(name = name) + ) + if (edit) fileEdit(file) +} + +addExampleApp <- function(name) { + renderFile( + 'app.R', + 'templates/widget_app.R.txt', + 'example app', + list( + name = name, + capName = capitalize(name) + ) + ) +} diff --git a/inst/templates/input_app.R.txt b/inst/templates/input_app.R.txt new file mode 100644 index 0000000..151ac55 --- /dev/null +++ b/inst/templates/input_app.R.txt @@ -0,0 +1,16 @@ +library(shiny) +library(${package}) + +ui <- fluidPage( + titlePanel("reactR Input Example"), + ${name}Input("textInput"), + textOutput("textOutput") +) + +server <- function(input, output, session) { + output$textOutput <- renderText({ + sprintf("You entered: %s", input$textInput) + }) +} + +shinyApp(ui, server) diff --git a/inst/templates/input_js.txt b/inst/templates/input_js.txt new file mode 100644 index 0000000..14781a1 --- /dev/null +++ b/inst/templates/input_js.txt @@ -0,0 +1,7 @@ +import { reactShinyInput } from 'reactR'; + +const TextInput = ({ configuration, value, setValue }) => { + return setValue(e.target.value)}/>; +}; + +reactShinyInput('.${name}', '${package}.${name}', TextInput); diff --git a/inst/templates/input_r.txt b/inst/templates/input_r.txt new file mode 100644 index 0000000..037ffa9 --- /dev/null +++ b/inst/templates/input_r.txt @@ -0,0 +1,36 @@ +#' +#' +#' +#' +#' @importFrom shiny restoreInput +#' @importFrom reactR createReactShinyInput +#' @importFrom htmltools htmlDependency tags +#' +#' @export +${name}Input <- function(inputId, default = "") { + reactR::createReactShinyInput( + inputId, + "${name}", + htmltools::htmlDependency( + name = "${name}-input", + version = "1.0.0", + src = "www/${package}/${name}", + package = "${package}", + script = "${name}.js" + ), + default, + list(), + htmltools::tags$span + ) +} + +#' +#' +#' +#' +#' @export +update${capName}Input <- function(session, inputId, value, configuration = NULL) { + message <- list(value = value) + if (!is.null(configuration)) message$configuration <- configuration + session$sendInputMessage(inputId, message); +} diff --git a/inst/templates/widget_package.json.txt b/inst/templates/package.json.txt similarity index 100% rename from inst/templates/widget_package.json.txt rename to inst/templates/package.json.txt diff --git a/inst/templates/widget_webpack.config.js.txt b/inst/templates/webpack.config.js.txt similarity index 68% rename from inst/templates/widget_webpack.config.js.txt rename to inst/templates/webpack.config.js.txt index 4161d75..bcfbb58 100644 --- a/inst/templates/widget_webpack.config.js.txt +++ b/inst/templates/webpack.config.js.txt @@ -1,15 +1,17 @@ var path = require('path'); module.exports = { - entry: path.join(__dirname, 'srcjs', '${name}.js'), + mode: 'development', + entry: path.join(__dirname, 'srcjs', '${name}.jsx'), output: { - path: path.join(__dirname, 'inst', 'htmlwidgets'), + path: path.join(__dirname, 'inst', 'www', '${package}', '${name}'), + path: path.join(__dirname, '${outputPath}'), filename: '${name}.js' }, module: { rules: [ { - test: /\.js$/, + test: /\.jsx?$/, loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'] diff --git a/inst/templates/widget_yaml.txt b/inst/templates/widget_yaml.txt new file mode 100644 index 0000000..82bc4da --- /dev/null +++ b/inst/templates/widget_yaml.txt @@ -0,0 +1,8 @@ +# (uncomment to add a dependency) +# dependencies: +# - name: +# version: +# src: +# script: +# stylesheet: + diff --git a/inst/www/react-tools/react-tools.js b/inst/www/react-tools/react-tools.js index 514b484..c785223 100644 --- a/inst/www/react-tools/react-tools.js +++ b/inst/www/react-tools/react-tools.js @@ -86,145 +86,395 @@ /************************************************************************/ /******/ ({ +/***/ "./srcjs/input.js": +/*!************************!*\ + !*** ./srcjs/input.js ***! + \************************/ +/*! exports provided: reactShinyInput */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reactShinyInput", function() { return reactShinyInput; }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react"); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-dom */ "react-dom"); +/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var shiny__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! shiny */ "shiny"); +/* harmony import */ var shiny__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(shiny__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! jquery */ "jquery"); +/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_3__); +function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } + +function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } + +function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } + +function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } + +function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } + + + + + +/* + * This default receiveMessage implementation expects data to contain whole + * configuration and value properties. If either is present, it will be set and + * the component will be re-rendered. Because receiveMessage is typically used + * by input authors to perform incremental updates, this default implementation + * can be overriden by the user with the receiveMessage arguments to + * reactShinyInput. + */ + +function defaultReceiveMessage(el, _ref) { + var configuration = _ref.configuration, + value = _ref.value; + var dirty = false; + + if (configuration !== undefined) { + this.setInputConfiguration(el, configuration); + dirty = true; + } + + if (value !== undefined) { + this.setInputValue(el, value); + dirty = true; + } + + if (dirty) { + this.getCallback(el)(); + this.render(el); + } +} + +var defaultOptions = { + receiveMessage: defaultReceiveMessage +}; +/** + * Installs a new Shiny input binding based on a React component. + * + * @param {string} selector - jQuery selector that should identify the set of + * container elements within the scope argument of Shiny.InputBinding.find. + * @param {string} name - A name such as 'acme.FooInput' that should uniquely + * identify the component. + * @param {Object} component - React Component, either class or function. + * @param {Object} options - Additional configuration options. Supported + * options are: + * - receiveMessage: Implementation of Shiny.InputBinding to use in place of + * the default. Typically overridden as an optimization to perform + * incremental value updates. + */ + +function reactShinyInput(selector, name, component, options) { + options = Object.assign({}, defaultOptions, options); + shiny__WEBPACK_IMPORTED_MODULE_2___default.a.inputBindings.register(new ( + /*#__PURE__*/ + function (_Shiny$InputBinding) { + _inherits(_class, _Shiny$InputBinding); + + function _class() { + _classCallCheck(this, _class); + + return _possibleConstructorReturn(this, _getPrototypeOf(_class).apply(this, arguments)); + } + + _createClass(_class, [{ + key: "find", + + /* + * Methods override those in Shiny.InputBinding + */ + value: function find(scope) { + return jquery__WEBPACK_IMPORTED_MODULE_3___default()(scope).find(selector); + } + }, { + key: "getValue", + value: function getValue(el) { + return this.getInputValue(el); + } + }, { + key: "setValue", + value: function setValue(el, value) { + this.setInputValue(el, value); + this.getCallback(el)(); + this.render(el); + } + }, { + key: "initialize", + value: function initialize(el) { + jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).data('value', JSON.parse(jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).next().text())); + jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).data('configuration', JSON.parse(jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).next().next().text())); + } + }, { + key: "subscribe", + value: function subscribe(el, callback) { + jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).data('callback', callback); + this.render(el); + } + }, { + key: "unsubscribe", + value: function unsubscribe(el, callback) { + jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).removeData('callback'); + react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(null, el); + } + }, { + key: "receiveMessage", + value: function receiveMessage(el, data) { + options.receiveMessage.call(this, el, data); + } + /* + * Methods not present in Shiny.InputBinding but accessible to users + * through `this` in receiveMessage + * */ + + }, { + key: "getInputValue", + value: function getInputValue(el) { + return jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).data('value'); + } + }, { + key: "setInputValue", + value: function setInputValue(el, value) { + jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).data('value', value); + } + }, { + key: "getInputConfiguration", + value: function getInputConfiguration(el) { + return jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).data('configuration'); + } + }, { + key: "setInputConfiguration", + value: function setInputConfiguration(el, configuration) { + jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).data('configuration', configuration); + } + }, { + key: "getCallback", + value: function getCallback(el) { + return jquery__WEBPACK_IMPORTED_MODULE_3___default()(el).data('callback'); + } + }, { + key: "render", + value: function render(el) { + var element = react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(component, { + configuration: this.getInputConfiguration(el), + value: this.getValue(el), + setValue: this.setValue.bind(this, el) + }); + react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(element, el); + } + }]); + + return _class; + }(shiny__WEBPACK_IMPORTED_MODULE_2___default.a.InputBinding))(), name); +} + +/***/ }), + /***/ "./srcjs/react-tools.js": /*!******************************!*\ !*** ./srcjs/react-tools.js ***! \******************************/ -/*! no static exports found */ -/***/ (function(module, exports) { +/*! no exports provided */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { -function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var _widget__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./widget */ "./srcjs/widget.js"); +/* harmony import */ var _input__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./input */ "./srcjs/input.js"); -window.reactR = function () { - /** - * Recursively transforms tag, a JSON representation of an instance of a - * React component and its children, into a React element suitable for - * passing to ReactDOM.render. - * @param {Object} components - * @param {Object} tag - */ - function hydrate(components, tag) { - if (typeof tag === 'string') return tag; - - if (tag.name[0] === tag.name[0].toUpperCase() && !components.hasOwnProperty(tag.name)) { - throw new Error("Unknown component: " + tag.name); - } - var elem = components.hasOwnProperty(tag.name) ? components[tag.name] : tag.name, - args = [elem, tag.attribs]; +window.reactR = { + reactShinyInput: _input__WEBPACK_IMPORTED_MODULE_1__["reactShinyInput"], + reactWidget: _widget__WEBPACK_IMPORTED_MODULE_0__["reactWidget"], + hydrate: _widget__WEBPACK_IMPORTED_MODULE_0__["hydrate"] +}; - for (var i = 0; i < tag.children.length; i++) { - args.push(hydrate(components, tag.children[i])); - } +/***/ }), - return React.createElement.apply(React, args); +/***/ "./srcjs/widget.js": +/*!*************************!*\ + !*** ./srcjs/widget.js ***! + \*************************/ +/*! exports provided: hydrate, defaultOptions, mergeOptions, formatDimension, isTag, reactWidget */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "hydrate", function() { return hydrate; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defaultOptions", function() { return defaultOptions; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeOptions", function() { return mergeOptions; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "formatDimension", function() { return formatDimension; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isTag", function() { return isTag; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reactWidget", function() { return reactWidget; }); +function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +/** + * Recursively transforms tag, a JSON representation of an instance of a + * React component and its children, into a React element suitable for + * passing to ReactDOM.render. + * @param {Object} components + * @param {Object} tag + */ +function hydrate(components, tag) { + if (typeof tag === 'string') return tag; + + if (tag.name[0] === tag.name[0].toUpperCase() && !components.hasOwnProperty(tag.name)) { + throw new Error("Unknown component: " + tag.name); } - var defaultOptions = { - // The name of the property on the root tag to use for the width, if - // it's updated. - widthProperty: "width", - // The name of the property on the root tag to use for the height, if - // it's updated. - heightProperty: "height", - // Whether or not to append the string 'px' to the width and height - // properties when they change. - appendPx: false, - // Whether or not to dynamically update the width and height properties - // of the last known tag when the computed width and height change in - // the browser. - renderOnResize: false - }; - - function mergeOptions(options) { - var merged = {}; - - for (var k in defaultOptions) { - merged[k] = defaultOptions[k]; - } + var elem = components.hasOwnProperty(tag.name) ? components[tag.name] : tag.name, + args = [elem, tag.attribs]; - for (var k in options) { - if (!defaultOptions.hasOwnProperty(k)) { - throw new Error("Unrecognized option: " + k); - } + for (var i = 0; i < tag.children.length; i++) { + args.push(hydrate(components, tag.children[i])); + } - merged[k] = options[k]; - } + return React.createElement.apply(React, args); +} +var defaultOptions = { + // The name of the property on the root tag to use for the width, if + // it's updated. + widthProperty: "width", + // The name of the property on the root tag to use for the height, if + // it's updated. + heightProperty: "height", + // Whether or not to append the string 'px' to the width and height + // properties when they change. + appendPx: false, + // Whether or not to dynamically update the width and height properties + // of the last known tag when the computed width and height change in + // the browser. + renderOnResize: false +}; +function mergeOptions(options) { + var merged = {}; - return merged; + for (var k in defaultOptions) { + merged[k] = defaultOptions[k]; } - function formatDimension(dim, options) { - if (options.appendPx) { - return dim + 'px'; - } else { - return dim; + for (var k in options) { + if (!defaultOptions.hasOwnProperty(k)) { + throw new Error("Unrecognized option: " + k); } + + merged[k] = options[k]; } - function isTag(value) { - return _typeof(value) === 'object' && value.hasOwnProperty('name') && value.hasOwnProperty('attribs') && value.hasOwnProperty('children'); + return merged; +} +function formatDimension(dim, options) { + if (options.appendPx) { + return dim + 'px'; + } else { + return dim; } - /** - * Creates an HTMLWidget that is updated by rendering a React component. - * React component constructors are made available by specifying them by - * name in the components object. - * @param {string} name - * @param {string} type - * @param {Object} components - * @param {Object} options - */ - - - function reactWidget(name, type, components, options) { - var actualOptions = mergeOptions(options); - HTMLWidgets.widget({ - name: name, - type: type, - factory: function factory(el, width, height) { - var lastValue, - instance = {}, - renderValue = function renderValue(value) { - if (actualOptions.renderOnResize) { - // value.tag might be a primitive string, in which - // case there is no attribs property. - if (_typeof(value.tag) === 'object') { - value.tag.attribs[actualOptions["widthProperty"]] = formatDimension(width); - value.tag.attribs[actualOptions["heightProperty"]] = formatDimension(height); - } - - lastValue = value; - } +} +function isTag(value) { + return _typeof(value) === 'object' && value.hasOwnProperty('name') && value.hasOwnProperty('attribs') && value.hasOwnProperty('children'); +} +/** + * Creates an HTMLWidget that is updated by rendering a React component. + * React component constructors are made available by specifying them by + * name in the components object. + * @param {string} name + * @param {string} type + * @param {Object} components + * @param {Object} options + */ - this.instance.component = ReactDOM.render(hydrate(components, value.tag), el); - }; - - return { - instance: instance, - renderValue: renderValue, - resize: function resize(newWidth, newHeight) { - if (actualOptions.renderOnResize) { - width = newWidth; - height = newHeight; - renderValue(lastValue); - } +function reactWidget(name, type, components, options) { + var actualOptions = mergeOptions(options); + window.HTMLWidgets.widget({ + name: name, + type: type, + factory: function factory(el, width, height) { + var lastValue, + instance = {}, + renderValue = function renderValue(value) { + if (actualOptions.renderOnResize) { + // value.tag might be a primitive string, in which + // case there is no attribs property. + if (_typeof(value.tag) === 'object') { + value.tag.attribs[actualOptions["widthProperty"]] = formatDimension(width); + value.tag.attribs[actualOptions["heightProperty"]] = formatDimension(height); } - }; - } - }); - } - return { - reactWidget: reactWidget, - hydrate: hydrate, - __internal: { - defaultOptions: defaultOptions, - mergeOptions: mergeOptions, - formatDimension: formatDimension, - isTag: isTag + lastValue = value; + } // with functional stateless components this will be null + // see https://reactjs.org/docs/react-dom.html#render for more details + + + this.instance.component = ReactDOM.render(hydrate(components, value.tag), el); + }; + + return { + instance: instance, + renderValue: renderValue, + resize: function resize(newWidth, newHeight) { + if (actualOptions.renderOnResize) { + width = newWidth; + height = newHeight; + renderValue(lastValue); + } + } + }; } - }; -}(); + }); +} + +/***/ }), + +/***/ "jquery": +/*!********************************!*\ + !*** external "window.jQuery" ***! + \********************************/ +/*! no static exports found */ +/***/ (function(module, exports) { + +module.exports = window.jQuery; + +/***/ }), + +/***/ "react": +/*!*******************************!*\ + !*** external "window.React" ***! + \*******************************/ +/*! no static exports found */ +/***/ (function(module, exports) { + +module.exports = window.React; + +/***/ }), + +/***/ "react-dom": +/*!**********************************!*\ + !*** external "window.ReactDOM" ***! + \**********************************/ +/*! no static exports found */ +/***/ (function(module, exports) { + +module.exports = window.ReactDOM; + +/***/ }), + +/***/ "shiny": +/*!*******************************!*\ + !*** external "window.Shiny" ***! + \*******************************/ +/*! no static exports found */ +/***/ (function(module, exports) { + +module.exports = window.Shiny; /***/ }) diff --git a/inst/www/react-tools/react-tools.js.map b/inst/www/react-tools/react-tools.js.map index 6766dba..b4e27bf 100644 --- a/inst/www/react-tools/react-tools.js.map +++ b/inst/www/react-tools/react-tools.js.map @@ -1 +1 @@ -{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./srcjs/react-tools.js"],"names":["window","reactR","hydrate","components","tag","name","toUpperCase","hasOwnProperty","Error","elem","args","attribs","i","children","length","push","React","createElement","apply","defaultOptions","widthProperty","heightProperty","appendPx","renderOnResize","mergeOptions","options","merged","k","formatDimension","dim","isTag","value","reactWidget","type","actualOptions","HTMLWidgets","widget","factory","el","width","height","lastValue","instance","renderValue","component","ReactDOM","render","resize","newWidth","newHeight","__internal"],"mappings":"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","file":"react-tools.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./srcjs/react-tools.js\");\n","window.reactR = (function () {\r\n /**\r\n * Recursively transforms tag, a JSON representation of an instance of a\r\n * React component and its children, into a React element suitable for\r\n * passing to ReactDOM.render.\r\n * @param {Object} components\r\n * @param {Object} tag\r\n */\r\n function hydrate(components, tag) {\r\n if (typeof tag === 'string') return tag;\r\n if (tag.name[0] === tag.name[0].toUpperCase()\r\n && !components.hasOwnProperty(tag.name)) {\r\n throw new Error(\"Unknown component: \" + tag.name);\r\n }\r\n var elem = components.hasOwnProperty(tag.name) ? components[tag.name] : tag.name,\r\n args = [elem, tag.attribs];\r\n for (var i = 0; i < tag.children.length; i++) {\r\n args.push(hydrate(components, tag.children[i]));\r\n }\r\n return React.createElement.apply(React, args);\r\n }\r\n\r\n var defaultOptions = {\r\n // The name of the property on the root tag to use for the width, if\r\n // it's updated.\r\n widthProperty: \"width\",\r\n // The name of the property on the root tag to use for the height, if\r\n // it's updated.\r\n heightProperty: \"height\",\r\n // Whether or not to append the string 'px' to the width and height\r\n // properties when they change.\r\n appendPx: false,\r\n // Whether or not to dynamically update the width and height properties\r\n // of the last known tag when the computed width and height change in\r\n // the browser.\r\n renderOnResize: false\r\n };\r\n\r\n function mergeOptions(options) {\r\n var merged = {};\r\n for (var k in defaultOptions) {\r\n merged[k] = defaultOptions[k];\r\n }\r\n for (var k in options) {\r\n if (!defaultOptions.hasOwnProperty(k)) {\r\n throw new Error(\"Unrecognized option: \" + k);\r\n }\r\n merged[k] = options[k];\r\n }\r\n return merged;\r\n }\r\n\r\n function formatDimension(dim, options) {\r\n if (options.appendPx) {\r\n return dim + 'px';\r\n } else {\r\n return dim;\r\n }\r\n }\r\n\r\n function isTag(value) {\r\n return (typeof value === 'object')\r\n && value.hasOwnProperty('name')\r\n && value.hasOwnProperty('attribs')\r\n && value.hasOwnProperty('children');\r\n }\r\n\r\n /**\r\n * Creates an HTMLWidget that is updated by rendering a React component.\r\n * React component constructors are made available by specifying them by\r\n * name in the components object.\r\n * @param {string} name\r\n * @param {string} type\r\n * @param {Object} components\r\n * @param {Object} options\r\n */\r\n function reactWidget(name, type, components, options) {\r\n var actualOptions = mergeOptions(options);\r\n HTMLWidgets.widget({\r\n name: name,\r\n type: type,\r\n factory: function (el, width, height) {\r\n var lastValue,\r\n instance = {},\r\n renderValue = (function (value) {\r\n if (actualOptions.renderOnResize) {\r\n // value.tag might be a primitive string, in which\r\n // case there is no attribs property.\r\n if (typeof value.tag === 'object') {\r\n value.tag.attribs[actualOptions[\"widthProperty\"]] = formatDimension(width);\r\n value.tag.attribs[actualOptions[\"heightProperty\"]] = formatDimension(height);\r\n }\r\n lastValue = value;\r\n }\r\n this.instance.component = ReactDOM.render(hydrate(components, value.tag), el);\r\n });\r\n return {\r\n instance: instance,\r\n renderValue: renderValue,\r\n resize: function (newWidth, newHeight) {\r\n if (actualOptions.renderOnResize) {\r\n width = newWidth;\r\n height = newHeight;\r\n renderValue(lastValue);\r\n }\r\n }\r\n };\r\n }\r\n })\r\n }\r\n\r\n return {\r\n reactWidget: reactWidget,\r\n hydrate: hydrate,\r\n __internal: {\r\n defaultOptions: defaultOptions,\r\n mergeOptions: mergeOptions,\r\n formatDimension: formatDimension,\r\n isTag: isTag\r\n }\r\n };\r\n})()\r\n"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./srcjs/input.js","webpack:///./srcjs/react-tools.js","webpack:///./srcjs/widget.js","webpack:///external \"window.jQuery\"","webpack:///external \"window.React\"","webpack:///external \"window.ReactDOM\"","webpack:///external \"window.Shiny\""],"names":["defaultReceiveMessage","el","configuration","value","dirty","undefined","setInputConfiguration","setInputValue","getCallback","render","defaultOptions","receiveMessage","reactShinyInput","selector","name","component","options","Object","assign","Shiny","inputBindings","register","scope","$","find","getInputValue","data","JSON","parse","next","text","callback","removeData","ReactDOM","call","element","React","createElement","getInputConfiguration","getValue","setValue","bind","InputBinding","window","reactR","reactWidget","hydrate","components","tag","toUpperCase","hasOwnProperty","Error","elem","args","attribs","i","children","length","push","apply","widthProperty","heightProperty","appendPx","renderOnResize","mergeOptions","merged","k","formatDimension","dim","isTag","type","actualOptions","HTMLWidgets","widget","factory","width","height","lastValue","instance","renderValue","resize","newWidth","newHeight"],"mappings":"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","file":"react-tools.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./srcjs/react-tools.js\");\n","import React from 'react';\nimport ReactDOM from 'react-dom';\nimport Shiny from 'shiny';\nimport $ from 'jquery';\n\n/*\n * This default receiveMessage implementation expects data to contain whole\n * configuration and value properties. If either is present, it will be set and\n * the component will be re-rendered. Because receiveMessage is typically used\n * by input authors to perform incremental updates, this default implementation\n * can be overriden by the user with the receiveMessage arguments to\n * reactShinyInput.\n */\nfunction defaultReceiveMessage(el, { configuration, value }) {\n let dirty = false;\n if (configuration !== undefined) {\n this.setInputConfiguration(el, configuration);\n dirty = true;\n }\n if (value !== undefined) {\n this.setInputValue(el, value);\n dirty = true;\n }\n if (dirty) {\n this.getCallback(el)();\n this.render(el);\n }\n}\n\nconst defaultOptions = {\n receiveMessage: defaultReceiveMessage\n};\n\n/**\n * Installs a new Shiny input binding based on a React component.\n *\n * @param {string} selector - jQuery selector that should identify the set of\n * container elements within the scope argument of Shiny.InputBinding.find.\n * @param {string} name - A name such as 'acme.FooInput' that should uniquely\n * identify the component.\n * @param {Object} component - React Component, either class or function.\n * @param {Object} options - Additional configuration options. Supported\n * options are:\n * - receiveMessage: Implementation of Shiny.InputBinding to use in place of\n * the default. Typically overridden as an optimization to perform\n * incremental value updates.\n */\nexport function reactShinyInput(selector,\n name,\n component,\n options) {\n options = Object.assign({}, defaultOptions, options);\n Shiny.inputBindings.register(new class extends Shiny.InputBinding {\n\n /*\n * Methods override those in Shiny.InputBinding\n */\n\n find(scope) {\n return $(scope).find(selector);\n }\n getValue(el) {\n return this.getInputValue(el);\n }\n setValue(el, value) {\n this.setInputValue(el, value);\n this.getCallback(el)();\n this.render(el);\n }\n initialize(el) {\n $(el).data('value', JSON.parse($(el).next().text()));\n $(el).data('configuration', JSON.parse($(el).next().next().text()));\n }\n subscribe(el, callback) {\n $(el).data('callback', callback);\n this.render(el);\n }\n unsubscribe(el, callback) {\n $(el).removeData('callback');\n ReactDOM.render(null, el);\n }\n receiveMessage(el, data) {\n options.receiveMessage.call(this, el, data);\n }\n\n /*\n * Methods not present in Shiny.InputBinding but accessible to users\n * through `this` in receiveMessage\n * */\n\n getInputValue(el) {\n return $(el).data('value');\n }\n setInputValue(el, value) {\n $(el).data('value', value);\n }\n getInputConfiguration(el) {\n return $(el).data('configuration');\n }\n setInputConfiguration(el, configuration) {\n $(el).data('configuration', configuration);\n }\n getCallback(el) {\n return $(el).data('callback');\n }\n render(el) {\n const element = React.createElement(component, {\n configuration: this.getInputConfiguration(el),\n value: this.getValue(el),\n setValue: this.setValue.bind(this, el)\n });\n ReactDOM.render(element, el);\n }\n }, name);\n}\n\n","import { reactWidget, hydrate } from './widget';\nimport { reactShinyInput } from './input';\n\nwindow.reactR = {\n reactShinyInput: reactShinyInput,\n reactWidget: reactWidget,\n hydrate: hydrate\n};\n","/**\n * Recursively transforms tag, a JSON representation of an instance of a\n * React component and its children, into a React element suitable for\n * passing to ReactDOM.render.\n * @param {Object} components\n * @param {Object} tag\n */\nexport function hydrate(components, tag) {\n if (typeof tag === 'string') return tag;\n if (tag.name[0] === tag.name[0].toUpperCase()\n && !components.hasOwnProperty(tag.name)) {\n throw new Error(\"Unknown component: \" + tag.name);\n }\n var elem = components.hasOwnProperty(tag.name) ? components[tag.name] : tag.name,\n args = [elem, tag.attribs];\n for (var i = 0; i < tag.children.length; i++) {\n args.push(hydrate(components, tag.children[i]));\n }\n return React.createElement.apply(React, args);\n}\n\nexport const defaultOptions = {\n // The name of the property on the root tag to use for the width, if\n // it's updated.\n widthProperty: \"width\",\n // The name of the property on the root tag to use for the height, if\n // it's updated.\n heightProperty: \"height\",\n // Whether or not to append the string 'px' to the width and height\n // properties when they change.\n appendPx: false,\n // Whether or not to dynamically update the width and height properties\n // of the last known tag when the computed width and height change in\n // the browser.\n renderOnResize: false\n};\n\nexport function mergeOptions(options) {\n var merged = {};\n for (var k in defaultOptions) {\n merged[k] = defaultOptions[k];\n }\n for (var k in options) {\n if (!defaultOptions.hasOwnProperty(k)) {\n throw new Error(\"Unrecognized option: \" + k);\n }\n merged[k] = options[k];\n }\n return merged;\n}\n\nexport function formatDimension(dim, options) {\n if (options.appendPx) {\n return dim + 'px';\n } else {\n return dim;\n }\n}\n\nexport function isTag(value) {\n return (typeof value === 'object')\n && value.hasOwnProperty('name')\n && value.hasOwnProperty('attribs')\n && value.hasOwnProperty('children');\n}\n\n/**\n * Creates an HTMLWidget that is updated by rendering a React component.\n * React component constructors are made available by specifying them by\n * name in the components object.\n * @param {string} name\n * @param {string} type\n * @param {Object} components\n * @param {Object} options\n */\nexport function reactWidget(name, type, components, options) {\n var actualOptions = mergeOptions(options);\n window.HTMLWidgets.widget({\n name: name,\n type: type,\n factory: function (el, width, height) {\n var lastValue,\n instance = {},\n renderValue = (function (value) {\n if (actualOptions.renderOnResize) {\n // value.tag might be a primitive string, in which\n // case there is no attribs property.\n if (typeof value.tag === 'object') {\n value.tag.attribs[actualOptions[\"widthProperty\"]] = formatDimension(width);\n value.tag.attribs[actualOptions[\"heightProperty\"]] = formatDimension(height);\n }\n lastValue = value;\n }\n // with functional stateless components this will be null\n // see https://reactjs.org/docs/react-dom.html#render for more details\n this.instance.component = ReactDOM.render(hydrate(components, value.tag), el);\n });\n return {\n instance: instance,\n renderValue: renderValue,\n resize: function (newWidth, newHeight) {\n if (actualOptions.renderOnResize) {\n width = newWidth;\n height = newHeight;\n renderValue(lastValue);\n }\n }\n };\n }\n })\n}\n\n","module.exports = window.jQuery;","module.exports = window.React;","module.exports = window.ReactDOM;","module.exports = window.Shiny;"],"sourceRoot":""} \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js index 6994c7a..c727f38 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -3,13 +3,24 @@ const webpackConfig = require('./webpack.config.js'); module.exports = function (config) { config.set({ frameworks: ['mocha', 'chai', 'source-map-support'], - files: ['inst/www/react-tools/react-tools.js', 'js-tests/js-tests.jsx'], + files: ['srcjs/react-tools.js', 'js-tests/js-tests.jsx'], preprocessors: { 'js-tests/*.js': ['webpack'], - 'js-tests/*.jsx': ['webpack'] + 'js-tests/*.jsx': ['webpack'], + 'srcjs/*.js': ['webpack'] }, webpack: { - module: webpackConfig.module + module: webpackConfig.module, + externals: { + /** + * In tests, react and react-dom are provided internally. + * The following libraries are not part of the testing environment, + * but are provided here as external so that webpack builds. + **/ + 'jquery': 'window.jQuery', + 'shiny': 'window.Shiny', + 'htmlwidgets': 'window.HTMLWidgets' + } }, webpackMiddleware: { stats: 'errors-only' @@ -23,4 +34,4 @@ module.exports = function (config) { // singleRun: false, // Karma captures browsers, runs the tests and exits concurrency: Infinity }) -} +} diff --git a/man/createReactShinyInput.Rd b/man/createReactShinyInput.Rd new file mode 100644 index 0000000..5bfdd52 --- /dev/null +++ b/man/createReactShinyInput.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/reacttools.R +\name{createReactShinyInput} +\alias{createReactShinyInput} +\title{Create a React-based input} +\usage{ +createReactShinyInput(inputId, class, dependencies, default = NULL, + configuration = list(), container = htmltools::tags$div) +} +\arguments{ +\item{inputId}{The \code{input} slot that will be used to access the value.} + +\item{class}{Space-delimited list of CSS class names that should identify +this input type in the browser.} + +\item{dependencies}{HTML dependencies to include in addition to those +supporting React. Must contain at least one dependency, that of the input's +implementation.} + +\item{default}{Initial value.} + +\item{configuration}{Static configuration data.} + +\item{container}{Function to generate an HTML element to contain the input.} +} +\value{ +Shiny input suitable for inclusion in a UI. +} +\description{ +Create a React-based input +} +\examples{ +myInput <- function(inputId, default = "") { + # The value of createReactShinyInput should be returned from input constructor functions. + createReactShinyInput( + inputId, + "myinput", + # At least one htmlDependency must be provided -- the JavaScript implementation of the input. + htmlDependency( + name = "my-input", + version = "1.0.0", + src = "www/mypackage/myinput", + package = "mypackage", + script = "myinput.js" + ), + default + ) +} +} diff --git a/man/scaffoldReactShinyInput.Rd b/man/scaffoldReactShinyInput.Rd new file mode 100644 index 0000000..b0e9490 --- /dev/null +++ b/man/scaffoldReactShinyInput.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/scaffold_input.R +\name{scaffoldReactShinyInput} +\alias{scaffoldReactShinyInput} +\title{Create implementation scaffolding for a React.js-based Shiny input.} +\usage{ +scaffoldReactShinyInput(name, npmPkgs = NULL, edit = interactive()) +} +\arguments{ +\item{name}{Name of input} + +\item{npmPkgs}{Optional \href{https://npmjs.com/}{NPM} packages upon which +this input is based which will be used to populate \code{package.json}. +Should be a named list of names to +\href{https://docs.npmjs.com/files/package.json#dependencies}{versions}.} + +\item{edit}{Automatically open the input's source files after creating the +scaffolding.} +} +\description{ +Add the minimal code required to implement a React.js-based Shiny input to an +R package. +} +\note{ +This function must be executed from the root directory of the package + you wish to add the input to. +} diff --git a/man/scaffoldReactWidget.Rd b/man/scaffoldReactWidget.Rd index 4b8fb1c..0ccd69b 100644 --- a/man/scaffoldReactWidget.Rd +++ b/man/scaffoldReactWidget.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/scaffold.R +% Please edit documentation in R/scaffold_widget.R \name{scaffoldReactWidget} \alias{scaffoldReactWidget} \title{Create implementation scaffolding for a React.js-based HTML widget} diff --git a/srcjs/input.js b/srcjs/input.js new file mode 100644 index 0000000..4b02cc9 --- /dev/null +++ b/srcjs/input.js @@ -0,0 +1,116 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Shiny from 'shiny'; +import $ from 'jquery'; + +/* + * This default receiveMessage implementation expects data to contain whole + * configuration and value properties. If either is present, it will be set and + * the component will be re-rendered. Because receiveMessage is typically used + * by input authors to perform incremental updates, this default implementation + * can be overriden by the user with the receiveMessage arguments to + * reactShinyInput. + */ +function defaultReceiveMessage(el, { configuration, value }) { + let dirty = false; + if (configuration !== undefined) { + this.setInputConfiguration(el, configuration); + dirty = true; + } + if (value !== undefined) { + this.setInputValue(el, value); + dirty = true; + } + if (dirty) { + this.getCallback(el)(); + this.render(el); + } +} + +const defaultOptions = { + receiveMessage: defaultReceiveMessage +}; + +/** + * Installs a new Shiny input binding based on a React component. + * + * @param {string} selector - jQuery selector that should identify the set of + * container elements within the scope argument of Shiny.InputBinding.find. + * @param {string} name - A name such as 'acme.FooInput' that should uniquely + * identify the component. + * @param {Object} component - React Component, either class or function. + * @param {Object} options - Additional configuration options. Supported + * options are: + * - receiveMessage: Implementation of Shiny.InputBinding to use in place of + * the default. Typically overridden as an optimization to perform + * incremental value updates. + */ +export function reactShinyInput(selector, + name, + component, + options) { + options = Object.assign({}, defaultOptions, options); + Shiny.inputBindings.register(new class extends Shiny.InputBinding { + + /* + * Methods override those in Shiny.InputBinding + */ + + find(scope) { + return $(scope).find(selector); + } + getValue(el) { + return this.getInputValue(el); + } + setValue(el, value) { + this.setInputValue(el, value); + this.getCallback(el)(); + this.render(el); + } + initialize(el) { + $(el).data('value', JSON.parse($(el).next().text())); + $(el).data('configuration', JSON.parse($(el).next().next().text())); + } + subscribe(el, callback) { + $(el).data('callback', callback); + this.render(el); + } + unsubscribe(el, callback) { + $(el).removeData('callback'); + ReactDOM.render(null, el); + } + receiveMessage(el, data) { + options.receiveMessage.call(this, el, data); + } + + /* + * Methods not present in Shiny.InputBinding but accessible to users + * through `this` in receiveMessage + * */ + + getInputValue(el) { + return $(el).data('value'); + } + setInputValue(el, value) { + $(el).data('value', value); + } + getInputConfiguration(el) { + return $(el).data('configuration'); + } + setInputConfiguration(el, configuration) { + $(el).data('configuration', configuration); + } + getCallback(el) { + return $(el).data('callback'); + } + render(el) { + const element = React.createElement(component, { + configuration: this.getInputConfiguration(el), + value: this.getValue(el), + setValue: this.setValue.bind(this, el) + }); + ReactDOM.render(element, el); + } + }, name); +} + diff --git a/srcjs/react-tools.js b/srcjs/react-tools.js index 015a9d5..0c48508 100644 --- a/srcjs/react-tools.js +++ b/srcjs/react-tools.js @@ -1,122 +1,8 @@ -window.reactR = (function () { - /** - * Recursively transforms tag, a JSON representation of an instance of a - * React component and its children, into a React element suitable for - * passing to ReactDOM.render. - * @param {Object} components - * @param {Object} tag - */ - function hydrate(components, tag) { - if (typeof tag === 'string') return tag; - if (tag.name[0] === tag.name[0].toUpperCase() - && !components.hasOwnProperty(tag.name)) { - throw new Error("Unknown component: " + tag.name); - } - var elem = components.hasOwnProperty(tag.name) ? components[tag.name] : tag.name, - args = [elem, tag.attribs]; - for (var i = 0; i < tag.children.length; i++) { - args.push(hydrate(components, tag.children[i])); - } - return React.createElement.apply(React, args); - } - - var defaultOptions = { - // The name of the property on the root tag to use for the width, if - // it's updated. - widthProperty: "width", - // The name of the property on the root tag to use for the height, if - // it's updated. - heightProperty: "height", - // Whether or not to append the string 'px' to the width and height - // properties when they change. - appendPx: false, - // Whether or not to dynamically update the width and height properties - // of the last known tag when the computed width and height change in - // the browser. - renderOnResize: false - }; - - function mergeOptions(options) { - var merged = {}; - for (var k in defaultOptions) { - merged[k] = defaultOptions[k]; - } - for (var k in options) { - if (!defaultOptions.hasOwnProperty(k)) { - throw new Error("Unrecognized option: " + k); - } - merged[k] = options[k]; - } - return merged; - } - - function formatDimension(dim, options) { - if (options.appendPx) { - return dim + 'px'; - } else { - return dim; - } - } - - function isTag(value) { - return (typeof value === 'object') - && value.hasOwnProperty('name') - && value.hasOwnProperty('attribs') - && value.hasOwnProperty('children'); - } - - /** - * Creates an HTMLWidget that is updated by rendering a React component. - * React component constructors are made available by specifying them by - * name in the components object. - * @param {string} name - * @param {string} type - * @param {Object} components - * @param {Object} options - */ - function reactWidget(name, type, components, options) { - var actualOptions = mergeOptions(options); - HTMLWidgets.widget({ - name: name, - type: type, - factory: function (el, width, height) { - var lastValue, - instance = {}, - renderValue = (function (value) { - if (actualOptions.renderOnResize) { - // value.tag might be a primitive string, in which - // case there is no attribs property. - if (typeof value.tag === 'object') { - value.tag.attribs[actualOptions["widthProperty"]] = formatDimension(width); - value.tag.attribs[actualOptions["heightProperty"]] = formatDimension(height); - } - lastValue = value; - } - this.instance.component = ReactDOM.render(hydrate(components, value.tag), el); - }); - return { - instance: instance, - renderValue: renderValue, - resize: function (newWidth, newHeight) { - if (actualOptions.renderOnResize) { - width = newWidth; - height = newHeight; - renderValue(lastValue); - } - } - }; - } - }) - } - - return { - reactWidget: reactWidget, - hydrate: hydrate, - __internal: { - defaultOptions: defaultOptions, - mergeOptions: mergeOptions, - formatDimension: formatDimension, - isTag: isTag - } - }; -})() +import { reactWidget, hydrate } from './widget'; +import { reactShinyInput } from './input'; + +window.reactR = { + reactShinyInput: reactShinyInput, + reactWidget: reactWidget, + hydrate: hydrate +}; diff --git a/srcjs/widget.js b/srcjs/widget.js new file mode 100644 index 0000000..4f8df3d --- /dev/null +++ b/srcjs/widget.js @@ -0,0 +1,112 @@ +/** + * Recursively transforms tag, a JSON representation of an instance of a + * React component and its children, into a React element suitable for + * passing to ReactDOM.render. + * @param {Object} components + * @param {Object} tag + */ +export function hydrate(components, tag) { + if (typeof tag === 'string') return tag; + if (tag.name[0] === tag.name[0].toUpperCase() + && !components.hasOwnProperty(tag.name)) { + throw new Error("Unknown component: " + tag.name); + } + var elem = components.hasOwnProperty(tag.name) ? components[tag.name] : tag.name, + args = [elem, tag.attribs]; + for (var i = 0; i < tag.children.length; i++) { + args.push(hydrate(components, tag.children[i])); + } + return React.createElement.apply(React, args); +} + +export const defaultOptions = { + // The name of the property on the root tag to use for the width, if + // it's updated. + widthProperty: "width", + // The name of the property on the root tag to use for the height, if + // it's updated. + heightProperty: "height", + // Whether or not to append the string 'px' to the width and height + // properties when they change. + appendPx: false, + // Whether or not to dynamically update the width and height properties + // of the last known tag when the computed width and height change in + // the browser. + renderOnResize: false +}; + +export function mergeOptions(options) { + var merged = {}; + for (var k in defaultOptions) { + merged[k] = defaultOptions[k]; + } + for (var k in options) { + if (!defaultOptions.hasOwnProperty(k)) { + throw new Error("Unrecognized option: " + k); + } + merged[k] = options[k]; + } + return merged; +} + +export function formatDimension(dim, options) { + if (options.appendPx) { + return dim + 'px'; + } else { + return dim; + } +} + +export function isTag(value) { + return (typeof value === 'object') + && value.hasOwnProperty('name') + && value.hasOwnProperty('attribs') + && value.hasOwnProperty('children'); +} + +/** + * Creates an HTMLWidget that is updated by rendering a React component. + * React component constructors are made available by specifying them by + * name in the components object. + * @param {string} name + * @param {string} type + * @param {Object} components + * @param {Object} options + */ +export function reactWidget(name, type, components, options) { + var actualOptions = mergeOptions(options); + window.HTMLWidgets.widget({ + name: name, + type: type, + factory: function (el, width, height) { + var lastValue, + instance = {}, + renderValue = (function (value) { + if (actualOptions.renderOnResize) { + // value.tag might be a primitive string, in which + // case there is no attribs property. + if (typeof value.tag === 'object') { + value.tag.attribs[actualOptions["widthProperty"]] = formatDimension(width); + value.tag.attribs[actualOptions["heightProperty"]] = formatDimension(height); + } + lastValue = value; + } + // with functional stateless components this will be null + // see https://reactjs.org/docs/react-dom.html#render for more details + this.instance.component = ReactDOM.render(hydrate(components, value.tag), el); + }); + return { + instance: instance, + renderValue: renderValue, + resize: function (newWidth, newHeight) { + if (actualOptions.renderOnResize) { + width = newWidth; + height = newHeight; + renderValue(lastValue); + } + } + }; + } + }) +} + diff --git a/vignettes/input_app.jpg b/vignettes/input_app.jpg new file mode 100644 index 0000000..91398ab Binary files /dev/null and b/vignettes/input_app.jpg differ diff --git a/vignettes/input_sketchpicker.jpg b/vignettes/input_sketchpicker.jpg new file mode 100644 index 0000000..6fb2af4 Binary files /dev/null and b/vignettes/input_sketchpicker.jpg differ diff --git a/vignettes/input_sketchpicker.mp4 b/vignettes/input_sketchpicker.mp4 new file mode 100644 index 0000000..97b06dd Binary files /dev/null and b/vignettes/input_sketchpicker.mp4 differ diff --git a/vignettes/intro_htmlwidgets.Rmd b/vignettes/intro_htmlwidgets.Rmd index 107033f..aab0dda 100644 --- a/vignettes/intro_htmlwidgets.Rmd +++ b/vignettes/intro_htmlwidgets.Rmd @@ -49,7 +49,7 @@ usethis::create_package("~/sparklines") # Inject the widget templating withr::with_dir( "~/sparklines", - reactR::scaffoldReactWidget("sparklines", list("react-sparklines" = "^1.7.0")) + reactR::scaffoldReactWidget("sparklines", list("react-sparklines" = "^1.7.0"), edit = FALSE) ) ``` @@ -91,9 +91,7 @@ shiny::runApp() Alternatively, in RStudio, you can open `app.R` and press `Ctrl-Shift-Enter` (`Cmd-Shift-Enter` on macOS). You should see something like the following appear in the Viewer pane: -```{r echo=FALSE} -knitr::include_graphics('./widget_app.jpg') -``` +![](./widget_app.jpg) ## Authoring a React binding @@ -101,6 +99,8 @@ At this point, we've built some scaffolding for an htmlwidget powered by React. ### First, outline an interface +> Note that the examples in this section are just to demonstrate API possibilities and need not be pasted into any file. + Consider the following example taken from the [react-sparklines documentation](http://borisyankov.github.io/react-sparklines/). ```js @@ -182,7 +182,7 @@ sparklines <- function(data, ..., width = NULL, height = NULL) { } ``` -At this point, we define functions that make it easy for the user to create the other components by adding these to `R/reactSparklines.R` +At this point, we define functions that make it easy for the user to create the other components by adding these to `R/sparklines.R` ```{r} #' @export @@ -270,6 +270,8 @@ shinyApp(ui, server) Now, when you run `shiny::runApp()`, you should see your react-based htmlwidget rendering in **shiny** app! +![](./widget_app_improved.jpg) + ## Further learning This tutorial walked you through the steps taken you create an R interface to the react-sparklines library. The full example package is accessible at . Our intention is keep creating example packages under the organization, so head there if you'd like to see other examples of interfacing with React. diff --git a/vignettes/intro_inputs.Rmd b/vignettes/intro_inputs.Rmd new file mode 100644 index 0000000..6062cbb --- /dev/null +++ b/vignettes/intro_inputs.Rmd @@ -0,0 +1,259 @@ +--- +title: "Authoring inputs powered by React with reactR" +author: + - Alan Dipert +date: "`r Sys.Date()`" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Shiny inputs with reactR} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, echo=FALSE, include=FALSE} +knitr::opts_chunk$set(eval = FALSE) +``` + +[Shiny](http://shiny.rstudio.com/) comes with a large library of input +[widgets](https://shiny.rstudio.com/gallery/widget-gallery.html) for collecting +input from the user and conveying input data to R. + +If you want a kind of input *not* provided by Shiny — like a color picker, +or a different kind of slider — you've always been able to build your own. +Shiny's input system is +[extensible](https://shiny.rstudio.com/articles/building-inputs.html). All +that's required is an understanding of certain conventions and a little custom +JavaScript. + +reactR provides additional tools to ease the creation of new Shiny inputs +implemented using React. In the following tutorial, we will demonstrate these +tools by implementing a new Shiny color picker input that wraps the +[react-color](https://github.com/casesandberg/react-color) library. + +## Software pre-requisites + +In order to develop a **reactR** Shiny input, you'll need to install R and +optionally RStudio. If you're on Windows, you should also install +[Rtools](https://cran.r-project.org/bin/windows/Rtools/). + +> For an excellent general introduction to R package concepts, check out the [R +> packages](http://r-pkgs.had.co.nz/) online book. + +In addition, you'll need to install the following JavaScript tools on your +machine: + +* [Node.js](https://nodejs.org): JavaScript engine and runtime for development + outside of browsers. Provides the `node` and `npm` commands. +* [Yarn](https://yarnpkg.com/en/): Command-line dependency management tool, + provides the `yarn` command. + +To follow along in this vignette, you'll also need the following R packages: + +```{r} +install.packages(c("shiny", "devtools", "usethis", "reactR")) +``` + +## Scaffolding + +To create a new widget you can call `scaffoldReactShinyInput` to generate the basic +structure and build configuration. This function will: + +* Create the .R, .js, and .json files required by your input; +* If provided, take an [npm](https://www.npmjs.com/) package name and version as + a named list with `name` and `version` elements. For example, the npm package + `foo` at version `^1.2.0` would be expressed as `list(name = "foo", version = + "^1.2.0")`. The package, if provided, will be added to the new widget's + `package.json` as a build dependency. + +The following R code will create an R package named **colorpicker**, then +provide the templating for creating an input powered by the +`react-color` library on npm: + +```{r} +# Create the R package +usethis::create_package("~/colorpicker") +# Scaffold initial input implementation files +withr::with_dir( + "~/colorpicker", + reactR::scaffoldReactShinyInput("colorpicker", list("react-color" = "^2.17.0"), edit = FALSE) +) +``` + +## Building and installing + +### Building the JavaScript + +The next step is to navigate to the newly-created `colorpicker` project and run +the following commands in the terminal: + +``` +yarn install +yarn run webpack +``` + +* `yarn install` downloads all of the dependencies listed in `package.json` and + creates a new file, `yarn.lock`. You should add this file to revision control. + It will be updated whenever you change dependencies and run `yarn install`. + **Note: you only need to run it after modifying package.json**. For further + documentation on `yarn install`, see the [yarn + documentation](https://yarnpkg.com/lang/en/docs/cli/install/). + +* `yarn run webpack` compiles the [modern JavaScript](https://babeljs.io/docs/en/babel-preset-env) + with [JSX](https://babeljs.io/docs/en/babel-preset-react) source file at `srcjs/colorpicker.jsx` into + `www/colorpicker/colorpicker/colorpicker.js`. The latter file is the one + actually used by the R package and includes all the relevant JavaScript + dependencies in a dialect of JavaScript that most browsers understand. + +`yarn run webpack` is not strictly a `yarn` command. In fact, `yarn run` simply +delegates to the [webpack](https://webpack.js.org/) program. Webpack's +configuration is generated by `scaffoldReactShinyInput` in the file +`webpack.config.js`, but you can always change this configuration and/or modify +the `yarn run webpack` command to suit your needs. + +### Installing the R package + +Now that the input's JavaScript is compiled, go ahead and install the R +package: + +```{r} +devtools::document() +devtools::install(quick = TRUE) +``` + +In RStudio, you can use the keyboard shortcuts `Ctrl-Shift-D` and +`Ctrl-Shift-B` to document and build the package. (On macOS, the shortcuts are +`Cmd-Shift-D` and `Cmd-Shift-B`) + +## Run the included demo + +Now that the input's JavaScript is compiled, and the R package is installed, +run `app.R` to see a demo in action: + +```{r} +shiny::runApp() +``` + +In RStudio, you can open `app.R` and press `Ctrl-Shift-Enter` +(`Cmd-Shift-Enter` on macOS). You should see something like the following appear +in the Viewer pane: + +![](./input_app.jpg) + +## Authoring a React input + +At this point, we have a working (if simple) React-powered text input. +Let's modify it to create an interface to the `react-color` library. + +### Connecting Shiny with React + +Consider the following example taken from the [react-color +documentation](http://casesandberg.github.io/react-color/). + +```js +import React from 'react'; +import { SketchPicker } from 'react-color'; + +class Component extends React.Component { + + render() { + return ; + } +} +``` + +That JavaScript code produces a `SketchPicker`-type interface that looks like +this: + +![](./input_sketchpicker.jpg) + +However, that example doesn't demonstrate a way to default to a particular +color, or a way to cause something to happen when the color changes. To +accomplish these, `react-color` components can [optionally +take](http://casesandberg.github.io/react-color/#api) the following +[props](https://reactjs.org/docs/components-and-props.html): + +* `color`: accepts a string of a hex color like `'#333'` +* `onChangeComplete`: accepts a function taking a single argument, the new + color, that will be called when the new color is selected + +These operations are conceptually similar enough to the API expected of Shiny +inputs that `reactR` can assist with integrating React components into Shiny +as inputs. + +It does so by introducing a convention for wrapping components like those +provided by `react-color` with an intermediate component that accepts these +props: + +* `configuration`: A configuration object containing data from R used to + parameterize the input's behavior +* `value`: The input's values over time, beginning with the default +* `setValue`: A function to call with the input's new value when one is created + +The `configuration` and `value` props are initially populated on the R side, as +arguments to the `createReactShinyInput` function inside the input's constructor +function. In the case of our newly-scaffolded input, that happens in +`R/colorpicker.R`. + +The `setValue` function is what causes new values to be sent to R, and also what +triggers the "intermediate" component to repaint itself. + +So, in order to make the components delivered by `react-color` accessible on the +R side, we must create our own intermediate component that wraps one of +`react-color`'s pickers. + +### Create intermediate component + +Open `srcjs/colorpicker.jsx` and paste the following in: + +```js +import { reactShinyInput } from 'reactR'; +import { SketchPicker } from 'react-color'; + +const PickerInput = ({ configuration, value, setValue }) => { + return ( + setValue(color.hex) } + /> + ); +}; + +reactShinyInput('.colorpicker', 'colorpicker', PickerInput); +``` + +The above code creates a new [function +component](https://reactjs.org/docs/components-and-props.html#function-and-class-components) +called `PickerInput` that expects the props supplied by reactR and renders a +parameterized `SketchPicker` from `react-color`. The `configuration` value is +not yet used. + +After saving the file, run `yarn run webpack` in the terminal and rebuild the +package. + +## Trying it out + +After rebuilding the JavaScript and the package, try running `app.R` again. You +should see something like this: + + + +When you select new colors, you should see the `textOutput` update accordingly. + +You might have noticed that the input showed up initially without a color +selected. That's because in `app.R` we didn't supply a `default` argument to the +`colorpickerInput` function inside our `ui`. + +Try replacing the call to `colorpickerInput` with this: +`colorpickerInput("textInput", default = "#a76161")` + +Now when you run the app, the color should start as a shade of red. + +## Further learning + +This tutorial walked you through the steps taken to wrap the `react-color` +library in a Shiny input. The full example package is accessible at +. Our intention is keep creating +example packages under the organization, so head +there if you'd like to see other examples of interfacing with React. diff --git a/yarn.lock b/yarn.lock index 324bee2..bba0957 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3758,15 +3758,15 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.7.0: - version "16.8.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.1.tgz#ec860f98853d09d39bafd3a6f1e12389d283dbb4" - integrity sha512-N74IZUrPt6UiDjXaO7UbDDFXeUXnVhZzeRLy/6iqqN1ipfjrhR60Bp5NuBK+rv3GMdqdIuwIl22u1SYwf330bg== +react-dom@^16.8.1: + version "16.8.3" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.3.tgz#ae236029e66210783ac81999d3015dfc475b9c32" + integrity sha512-ttMem9yJL4/lpItZAQ2NTFAbV7frotHk5DZEHXUOws2rMmrsvh1Na7ThGT0dTzUIl6pqTOi5tYREfL8AEna3lA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.1" + scheduler "^0.13.3" react-html-parser@^2.0.2: version "2.0.2" @@ -3780,15 +3780,15 @@ react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.1.tgz#a80141e246eb894824fb4f2901c0c50ef31d4cdb" integrity sha512-ioMCzVDWvCvKD8eeT+iukyWrBGrA3DiFYkXfBsVYIRdaREZuBjENG+KjrikavCLasozqRWTwFUagU/O4vPpRMA== -react@^16.7.0: - version "16.8.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.1.tgz#ae11831f6cb2a05d58603a976afc8a558e852c4a" - integrity sha512-wLw5CFGPdo7p/AgteFz7GblI2JPOos0+biSoxf1FPsGxWQZdN/pj6oToJs1crn61DL3Ln7mN86uZ4j74p31ELQ== +react@^16.8.1: + version "16.8.3" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.3.tgz#c6f988a2ce895375de216edcfaedd6b9a76451d9" + integrity sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.1" + scheduler "^0.13.3" "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" @@ -4007,10 +4007,10 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.1.tgz#1a217df1bfaabaf4f1b92a9127d5d732d85a9591" - integrity sha512-VJKOkiKIN2/6NOoexuypwSrybx13MY7NSy9RNt8wPvZDMRT1CW6qlpF5jXRToXNHz3uWzbm2elNpZfXfGPqP9A== +scheduler@^0.13.3: + version "0.13.3" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.3.tgz#bed3c5850f62ea9c716a4d781f9daeb9b2a58896" + integrity sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1"