├── .Rbuildignore ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── R ├── server.R └── widget.R ├── README.md ├── anyhtmlwidget.Rproj ├── inst └── htmlwidgets │ ├── anyhtmlwidget.js │ ├── anyhtmlwidget.yaml │ └── lib │ ├── index.css │ └── index.js ├── man ├── AnyHtmlWidget.Rd ├── anyhtmlwidget_output.Rd ├── render_anyhtmlwidget.Rd ├── the_anyhtmlwidget.Rd ├── widgetServer.Rd └── widgetUI.Rd ├── pkgdown └── _pkgdown.yml └── vignettes ├── basics.Rmd ├── session_info.Rmd └── shiny.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.BBSoptions$ 4 | ^.github$ 5 | ^pkgdown$ 6 | ^vignettes$ 7 | ^docs$ 8 | ^index\.md$ 9 | ^.ipynb_checkpoints$ 10 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: R 2 | 3 | on: [push, pull_request] 4 | 5 | 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | 11 | jobs: 12 | pre_deploy: 13 | runs-on: ubuntu-latest 14 | env: 15 | cache-version: 5 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up libraries for Ubuntu 19 | run: | 20 | sudo apt-get update 21 | sudo apt-get install -y libsodium-dev libharfbuzz-dev libfribidi-dev libcurl4-openssl-dev texlive-latex-base texlive-fonts-extra pandoc libmagick++-dev libhdf5-dev 22 | - name: Set up R 4.0 23 | uses: r-lib/actions/setup-r@v2 24 | with: 25 | r-version: 4.0 26 | - name: Set CRAN mirror 27 | run: | 28 | cat("\noptions(repos=structure(c(CRAN=\"https://cran.rstudio.com\")))\n", file = "~/.Rprofile", append = TRUE) 29 | shell: Rscript {0} 30 | - name: Get R and OS version 31 | id: get-version 32 | run: | 33 | cat("::set-output name=os-version::", sessionInfo()$running, "\n", sep = "") 34 | cat("::set-output name=r-version::", R.Version()$version.string, "\n", sep = "") 35 | cat("::endgroup::\n") 36 | shell: Rscript {0} 37 | - name: Cache dependencies 38 | id: cache-deps 39 | uses: actions/cache@v2 40 | with: 41 | path: ${{ env.R_LIBS_USER }}/* 42 | key: ${{ hashFiles('DESCRIPTION') }}-${{ steps.get-version.outputs.os-version }}-${{ steps.get-version.outputs.r-version }}-${{ env.cache-version }}-deps 43 | - name: Install dependencies 44 | if: steps.cache-deps.outputs.cache-hit != 'true' 45 | run: | 46 | install.packages(c("devtools", "remotes", "rcmdcheck", "covr")) 47 | remotes::install_deps(dependencies = TRUE) 48 | shell: Rscript {0} 49 | env: 50 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 51 | - name: rcmdcheck 52 | run: | 53 | rcmdcheck::rcmdcheck( 54 | error_on = "error", # TODO: switch back to "warning" 55 | check_dir = "check" 56 | ) 57 | shell: Rscript {0} 58 | env: 59 | _R_CHECK_FORCE_SUGGESTS_: false 60 | - name: Run coverage report 61 | run: | 62 | covr::package_coverage() 63 | shell: Rscript {0} 64 | #- name: Downgrade pkgdown 65 | # run: | 66 | # remotes::install_version("pkgdown", "2.0.3") 67 | # shell: Rscript {0} 68 | - name: Build docs 69 | run: | 70 | Rscript -e 'devtools::document(); pkgdown::build_site(new_process = FALSE)' 71 | touch docs/.nojekyll 72 | - uses: actions/upload-pages-artifact@v1 73 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 74 | with: 75 | path: ./docs 76 | 77 | deploy: 78 | runs-on: ubuntu-latest 79 | needs: pre_deploy 80 | environment: 81 | name: github-pages 82 | url: ${{ steps.deployment.outputs.page_url }} 83 | steps: 84 | - name: Deploy to GitHub Pages 85 | id: deployment 86 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 87 | uses: actions/deploy-pages@v1 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rhistory 2 | .Rproj.user/ 3 | .DS_Store 4 | docs/ 5 | .ipynb_checkpoints/ 6 | .RData 7 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: anyhtmlwidget 2 | Type: Package 3 | Title: Create interactive widgets using EcmaScript Modules 4 | Version: 0.1.0 5 | Authors@R: c( 6 | person( 7 | given = "Mark", 8 | family = "Keller", 9 | email = "mark_keller@g.harvard.edu", 10 | role = c("cre", "aut"), 11 | comment = c(ORCID = "0000-0003-3003-874X") 12 | ) 13 | ) 14 | Description: Anyhtmlwidget brings concepts from 15 | AnyWidget to R. 16 | License: MIT + file LICENSE 17 | BugReports: https://github.com/keller-mark/anyhtmlwidget/issues 18 | URL: https://github.com/keller-mark/anyhtmlwidget 19 | Depends: R (>= 4.0.0) 20 | Imports: 21 | htmlwidgets, 22 | R6, 23 | shiny, 24 | httpuv, 25 | jsonlite 26 | Encoding: UTF-8 27 | LazyData: true 28 | Roxygen: list(markdown = TRUE) 29 | RoxygenNote: 7.3.1 30 | Suggests: 31 | testthat, 32 | bslib, 33 | pkgdown, 34 | knitr, 35 | rmarkdown 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2024 2 | COPYRIGHT HOLDER: Mark Keller 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 Mark Keller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(AnyHtmlWidget) 4 | export(widgetServer) 5 | export(widgetUI) 6 | -------------------------------------------------------------------------------- /R/server.R: -------------------------------------------------------------------------------- 1 | start_server <- function(w, host = "0.0.0.0", port = 8080) { 2 | app <- list( 3 | onWSOpen = function(ws) { 4 | # The ws object is a WebSocket object 5 | cat("Server connection opened.\n") 6 | 7 | w$.on_change(function(key, new_val) { 8 | msg_list <- list( 9 | type = jsonlite::unbox("on_change"), 10 | payload = list( 11 | key = jsonlite::unbox(key), 12 | value = jsonlite::unbox(new_val) 13 | ) 14 | ) 15 | ws$send(jsonlite::toJSON(msg_list, auto_unbox = FALSE)) 16 | }) 17 | 18 | ws$onMessage(function(binary, message) { 19 | msg_list <- jsonlite::fromJSON(message, simplifyVector = FALSE, auto_unbox = FALSE) 20 | msg_type <- msg_list$type 21 | msg_val <- msg_list$payload 22 | 23 | if(msg_type == "on_save_changes") { 24 | for(key in names(msg_val)) { 25 | w$.set_value(key, msg_val[[key]], emit_change = FALSE) 26 | } 27 | } 28 | 29 | ret_val <- list(success = TRUE) 30 | ws$send(jsonlite::toJSON(ret_val, auto_unbox = FALSE)) 31 | }) 32 | ws$onClose(function() { 33 | cat("Server connection closed.\n") 34 | }) 35 | } 36 | ) 37 | 38 | s <- httpuv::startServer(host = host, port = port, app = app) 39 | return(s) 40 | } 41 | 42 | -------------------------------------------------------------------------------- /R/widget.R: -------------------------------------------------------------------------------- 1 | #' The internal function that creates the htmlwidget. 2 | #' @param esm The ES Module as a string. 3 | #' @param values The values that will be used for the initial Model state. 4 | #' @param ns_id Namespace ID, only used when in the Shiny module mode. Optional. 5 | #' @param width The width of the widget as a number or CSS string. Optional. 6 | #' @param height The height of the widget as a number or CSS string. Optional. 7 | #' @param port The port of the WebSocket server, when in dynamic mode. Optional. 8 | #' @param host The host of the WebSocket server, when in dynamic mode. Optional. 9 | #' @param element_id An element ID. Optional. 10 | #' @return The result of htmlwidgets::createWidget. 11 | #' 12 | #' @keywords internal 13 | the_anyhtmlwidget <- function(esm, values = NULL, ns_id = NULL, width = NULL, height = NULL, port = NULL, host = NULL, element_id = NULL) { 14 | params = list( 15 | esm = esm, 16 | values = values, 17 | ns_id = ns_id, 18 | port = port, 19 | host = host 20 | ) 21 | 22 | htmlwidgets::createWidget( 23 | 'anyhtmlwidget', 24 | params, 25 | width = width, 26 | height = height, 27 | package = 'anyhtmlwidget', 28 | elementId = element_id, 29 | sizingPolicy = htmlwidgets::sizingPolicy( 30 | viewer.padding = 0, 31 | browser.padding = 0, 32 | browser.fill = TRUE, 33 | viewer.fill = TRUE 34 | ) 35 | ) 36 | } 37 | 38 | #' Internal Shiny UI binding for anyhtmlwidget. 39 | #' 40 | #' @param output_id output variable to read from 41 | #' @param width,height Must be a valid CSS unit (like \code{'100\%'}, 42 | #' \code{'400px'}, \code{'auto'}) or a number, which will be coerced to a 43 | #' string and have \code{'px'} appended. 44 | #' @param expr An expression that generates a vitessce 45 | #' @param env The environment in which to evaluate \code{expr}. 46 | #' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This 47 | #' is useful if you want to save an expression in a variable. 48 | #' @return The Shiny UI element. 49 | #' 50 | #' @keywords internal 51 | anyhtmlwidget_output <- function(output_id, width = '100%', height = '400px'){ 52 | htmlwidgets::shinyWidgetOutput(output_id, 'anyhtmlwidget', width, height, package = 'anyhtmlwidget') 53 | } 54 | 55 | #' Internal Shiny server binding for anyhtmlwidget. 56 | #' 57 | #' @keywords internal 58 | render_anyhtmlwidget <- function(expr, env = parent.frame(), quoted = FALSE) { 59 | if (!quoted) { expr <- substitute(expr) } # force quoted 60 | htmlwidgets::shinyRenderWidget(expr, anyhtmlwidget_output, env, quoted = TRUE) 61 | } 62 | 63 | #' AnyHtmlWidget 64 | #' @title AnyHtmlWidget Class 65 | #' @docType class 66 | #' @description 67 | #' Class representing a widget. 68 | #' 69 | #' @rdname AnyHtmlWidget 70 | #' @export 71 | AnyHtmlWidget <- R6::R6Class("AnyHtmlWidget", 72 | lock_objects = FALSE, 73 | private = list( 74 | # TODO: prefix with dot 75 | # since R6 requires all items in 76 | # public, private, and active to have unique names 77 | .esm = NULL, 78 | .values = NULL, 79 | .mode = NULL, 80 | .change_handler = NULL, 81 | .server = NULL, 82 | .server_host = NULL, 83 | .server_port = NULL, 84 | .width = NULL, 85 | .height = NULL 86 | ), 87 | active = list(), 88 | public = list( 89 | #' @description 90 | #' Create a new widget instance. 91 | #' @param .esm The EcmaScript module as a string. 92 | #' @param .mode The widget mode. 93 | #' @param .width The widget width. Optional. 94 | #' @param .height The widget height. Optional. 95 | #' @param .commands TODO 96 | #' @param ... All other named arguments will be used to create active bindings on the instance. 97 | initialize = function(.esm, .mode, .width = NA, .height = NA, .commands = NA, ...) { 98 | private$.esm <- .esm 99 | private$.values <- list(...) 100 | 101 | if(is.na(.width)) { 102 | .width <- "100%" 103 | } 104 | if(is.na(.height)) { 105 | .height <- "100%" 106 | } 107 | 108 | private$.width <- .width 109 | private$.height <- .height 110 | 111 | private$.server_host <- "0.0.0.0" 112 | private$.server_port <- httpuv::randomPort(min = 8000, max = 9000, n = 1000) 113 | 114 | if(!.mode %in% c("static", "gadget", "shiny", "dynamic")) { 115 | stop("Invalid widget mode.") 116 | } 117 | private$.mode <- .mode 118 | 119 | active_env <- self$`.__enclos_env__`$`.__active__` 120 | 121 | # TODO: check that values is not NA 122 | for(key in names(private$.values)) { 123 | active_binding <- function(val) { 124 | if(missing(val)) { 125 | return(self$.get_value(key)) 126 | } else { 127 | self$.set_value(key, val) 128 | if(private$.mode == "static") { 129 | self$print() 130 | } 131 | } 132 | } 133 | active_env[[key]] <- active_binding 134 | makeActiveBinding(key, active_env[[key]], self) 135 | } 136 | self$`.__enclos_env__`$`.__active__` <- active_env 137 | }, 138 | #' @description 139 | #' Set a value. 140 | #' @param key The key of the value to set. 141 | #' @param val The new value. 142 | #' @param emit_change Should the .on_change handler be called? 143 | .set_value = function(key, val, emit_change = TRUE) { 144 | private$.values[[key]] <- val 145 | if(emit_change && !is.null(private$.change_handler)) { 146 | # Should this only call the callback if the current value is different than the new value? 147 | private$.change_handler(key, val) 148 | } 149 | }, 150 | #' @description 151 | #' Register a change handler to call if emit_change is TRUE in .set_value. 152 | #' @param callback A callback function to register. 153 | .on_change = function(callback) { 154 | private$.change_handler <- callback 155 | }, 156 | #' @description 157 | #' Get a particular value. 158 | #' @param key The key of the value to get. 159 | #' @returns The value. 160 | .get_value = function(key) { 161 | return(private$.values[[key]]) 162 | }, 163 | #' @description 164 | #' Get the ESM string. 165 | #' @returns The ESM string. 166 | .get_esm = function() { 167 | return(private$.esm) 168 | }, 169 | #' @description 170 | #' Get all widget values 171 | #' @returns List of values. 172 | .get_values = function() { 173 | return(private$.values) 174 | }, 175 | #' @description 176 | #' Get the widget width. 177 | #' @returns The width. 178 | .get_width = function() { 179 | return(private$.width) 180 | }, 181 | #' @description 182 | #' Get the widget height. 183 | #' @returns The height. 184 | .get_height = function() { 185 | return(private$.height) 186 | }, 187 | #' @description 188 | #' Get the server port. 189 | #' @returns The port number. 190 | .get_mode = function() { 191 | return(private$.mode) 192 | }, 193 | #' @description 194 | #' Set all values. TODO: is this ever used? 195 | #' @param new_values A list of new values. 196 | .set_values = function(new_values) { 197 | private$.values <- new_values 198 | }, 199 | #' @description 200 | #' Set the widget mode. 201 | #' @param mode The new widget mode. 202 | .set_mode = function(mode) { 203 | if(!mode %in% c("static", "gadget", "shiny", "dynamic")) { 204 | stop("Invalid widget mode.") 205 | } 206 | private$.mode <- mode 207 | }, 208 | #' @description 209 | #' Start the server, if not running. 210 | .start_server = function() { 211 | if(is.null(private$.server)) { 212 | private$.server <- start_server(self, host = private$.server_host, port = private$.server_port) 213 | } 214 | }, 215 | #' @description 216 | #' Stop the server, if running. 217 | .stop_server = function() { 218 | if(!is.null(private$.server)) { 219 | private$.server$stop() 220 | } 221 | }, 222 | #' @description 223 | #' Get the server hostname. 224 | #' @return The hostname as a string. 225 | .get_host = function() { 226 | return(private$.server_host) 227 | }, 228 | #' @description 229 | #' Get the server port. 230 | #' @returns The port number. 231 | .get_port = function() { 232 | return(private$.server_port) 233 | }, 234 | #' @description 235 | #' Custom print function for the R6 class. 236 | #' If mode is "shiny", falls back to original R6 print behavior. 237 | #' Otherwise, renders the widget. 238 | print = function() { 239 | if(private$.mode == "shiny") { 240 | # If Shiny mode, we just want to use the original R6 print behavior. 241 | # Reference: https://github.com/r-lib/R6/blob/507867875fdeaffbe7f7038291256b798f6bb042/R/print.R#L35C5-L35C36 242 | print(cat(format(self), sep = "\n")) 243 | } else { 244 | # Otherwise, we want to render the widget. 245 | self$render() 246 | } 247 | }, 248 | #' @description 249 | #' Render the widget. 250 | render = function() { 251 | if(private$.mode == "static") { 252 | invoke_static(self) 253 | } else if(private$.mode == "gadget") { 254 | invoke_gadget(self) 255 | } else if(private$.mode == "dynamic") { 256 | invoke_dynamic(self) 257 | } else { 258 | stop("render is meant for use with static, gadget, and dynamic modes") 259 | } 260 | } 261 | ) 262 | ) 263 | 264 | #' @keywords internal 265 | invoke_static <- function(w) { 266 | w <- the_anyhtmlwidget( 267 | esm = w$.get_esm(), 268 | values = w$.get_values(), 269 | width = w$.get_width(), 270 | height = w$.get_height() 271 | ) 272 | print(w) 273 | } 274 | 275 | #' @keywords internal 276 | invoke_dynamic <- function(w) { 277 | w$.start_server() 278 | w <- the_anyhtmlwidget( 279 | esm = w$.get_esm(), 280 | values = w$.get_values(), 281 | width = w$.get_width(), 282 | height = w$.get_height(), 283 | port = w$.get_port(), 284 | host = w$.get_host() 285 | ) 286 | print(w) 287 | } 288 | 289 | #' @keywords internal 290 | invoke_gadget <- function(w) { 291 | ui <- shiny::tagList( 292 | anyhtmlwidget_output(output_id = "my_widget", width = '100%', height = '100%') 293 | ) 294 | 295 | server <- function(input, output, session) { 296 | increment <- shiny::reactiveVal(0) 297 | 298 | shiny::observeEvent(input$anyhtmlwidget_on_save_changes, { 299 | # update values on w here 300 | for(key in names(input$anyhtmlwidget_on_save_changes)) { 301 | w$.set_value(key, input$anyhtmlwidget_on_save_changes[[key]], emit_change = FALSE) 302 | } 303 | increment(increment() + 1) 304 | }) 305 | 306 | # output$values <- renderPrint({ 307 | # increment() 308 | # w$.get_values() 309 | # }) 310 | # 311 | # observeEvent(input$go, { 312 | # w$count <- 999 313 | # increment(increment() + 1) 314 | # }) 315 | 316 | w$.on_change(function(key, new_val) { 317 | session$sendCustomMessage("anyhtmlwidget_on_change", list(key = key, value = new_val)) 318 | }) 319 | 320 | output$my_widget <- render_anyhtmlwidget(expr = { 321 | the_anyhtmlwidget(esm = w$.get_esm(), values = w$.get_values(), width = w$.get_width(), height = w$.get_height()) 322 | }) 323 | } 324 | 325 | shiny::runGadget(ui, server) 326 | } 327 | 328 | #' Shiny module UI for anyhtmlwidgets. 329 | #' 330 | #' @param id The output ID. 331 | #' @param width The widget width. Optional. 332 | #' @param height The widget height. Optional. 333 | #' 334 | #' @export 335 | widgetUI <- function(id, width = '100%', height = '400px') { 336 | ns <- shiny::NS(id) 337 | anyhtmlwidget_output(output_id = ns("widget"), width = width, height = height) 338 | } 339 | 340 | #' Shiny module server for anyhtmlwidgets. 341 | #' 342 | #' @param id The matching output ID used in the Shiny UI. 343 | #' @param w The widget instance. 344 | #' @returns reactiveValues corresponding to the widget's active bindings. 345 | #' 346 | #' @export 347 | widgetServer <- function(id, w) { 348 | ns <- shiny::NS(id) 349 | shiny::moduleServer( 350 | id, 351 | function(input, output, session) { 352 | initial_values <- w$.get_values() 353 | rv <- do.call(shiny::reactiveValues, initial_values) 354 | 355 | shiny::observeEvent(input$anyhtmlwidget_on_save_changes, { 356 | # update values on w here 357 | for(key in names(input$anyhtmlwidget_on_save_changes)) { 358 | rv[[key]] <- input$anyhtmlwidget_on_save_changes[[key]] 359 | w$.set_value(key, input$anyhtmlwidget_on_save_changes[[key]], emit_change = FALSE) 360 | } 361 | }) 362 | 363 | for(key in names(initial_values)) { 364 | shiny::observeEvent(rv[[key]], { 365 | session$sendCustomMessage(ns("anyhtmlwidget_on_change"), list(key = key, value = rv[[key]])) 366 | }) 367 | } 368 | 369 | output$widget <- render_anyhtmlwidget(expr = { 370 | the_anyhtmlwidget(esm = w$.get_esm(), values = initial_values, width = w$.get_width(), height = w$.get_height(), ns_id = id) 371 | }) 372 | 373 | return(rv) 374 | } 375 | ) 376 | } 377 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anyhtmlwidget 2 | 3 | Bringing core concepts from [anywidget](https://github.com/manzt/anywidget) to R. 4 | 5 | - Define widget as a JavaScript EcmaScript Module (ESM) string in R 6 | - Access state from JS using the same AnyWidget `model` API. 7 | - Bidirectional communication (R <-> JS) 8 | 9 | ## Installation 10 | 11 | ```R 12 | devtools::install_github("keller-mark/anyhtmlwidget") 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```R 18 | library(anyhtmlwidget) 19 | 20 | esm <- " 21 | function render({ el, model }) { 22 | let count = () => model.get('count'); 23 | let btn = document.createElement('button'); 24 | btn.innerHTML = `count button ${count()}`; 25 | btn.addEventListener('click', () => { 26 | model.set('count', count() + 1); 27 | model.save_changes(); 28 | }); 29 | model.on('change:count', () => { 30 | btn.innerHTML = `count is ${count()}`; 31 | }); 32 | el.appendChild(btn); 33 | } 34 | export default { render }; 35 | " 36 | 37 | widget <- AnyHtmlWidget$new(.esm = esm, .mode = "dynamic", count = 1) 38 | widget$render() 39 | ``` 40 | 41 | Setting a value will cause a re-render: 42 | 43 | ```R 44 | widget$count <- 2 45 | ``` 46 | 47 | Access the latest values: 48 | 49 | ```R 50 | widget$count 51 | # [1] 2 52 | ``` 53 | 54 | ## Modes 55 | 56 | - `dynamic`: Bidirectional communication via background WebSocket server. Does not block R console. 57 | - `gadget`: Bidirectional communication via Shiny running as a Gadget. Blocks R console. 58 | - `static`: Unidirectional communication (R -> JS). Does not block R console. 59 | - `shiny`: Bidirectional communication. Use when embedding a widget in a Shiny app. 60 | 61 | 62 | -------------------------------------------------------------------------------- /anyhtmlwidget.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | -------------------------------------------------------------------------------- /inst/htmlwidgets/anyhtmlwidget.js: -------------------------------------------------------------------------------- 1 | class CallbackRegistry { 2 | /** @type {Record} */ 3 | #callbacks = {}; 4 | /** 5 | * @param {string} name 6 | * @param {Function} callback 7 | */ 8 | add(name, callback) { 9 | if (!this.#callbacks[name]) { 10 | this.#callbacks[name] = []; 11 | } 12 | this.#callbacks[name].push(callback); 13 | } 14 | /** 15 | * @param {string} name 16 | * @param {Function} [callback] - a specific callback to remove 17 | * @returns {Function[]} - the removed callbacks 18 | */ 19 | remove(name, callback) { 20 | if (!this.#callbacks[name]) { 21 | return []; 22 | } 23 | const callbacks = this.#callbacks[name]; 24 | if (callback) { 25 | // Remove a specific callback 26 | return this.#callbacks[name] = callbacks.filter((cb) => cb !== callback); 27 | } 28 | // Remove all callbacks 29 | this.#callbacks[name] = []; 30 | return callbacks; 31 | } 32 | } 33 | 34 | /** 35 | * An R-backed implementation of the @anywidget/types AnyModel interface. 36 | * 37 | * @see {@link https://github.com/manzt/anywidget/tree/main/packages/types} 38 | */ 39 | class AnyModel { 40 | /** @type {Record} */ 41 | #state; 42 | /** @type {string} */ 43 | #ns_id; 44 | /** @type {WebSocket | undefined} */ 45 | #ws = undefined; 46 | /** @type {EventTarget} */ 47 | #target = new EventTarget(); 48 | /** @type {CallbackRegistry} */ 49 | #callbacks = new CallbackRegistry(); 50 | /** @type {Set} */ 51 | #unsavedKeys = new Set(); 52 | 53 | /** 54 | * @param {Record} state - initial model state 55 | * @param {string} ns_id - the Shiny namespace ID 56 | * @param {WebSocket} [ws] - a WebSocket connection 57 | */ 58 | constructor(state, ns_id, ws) { 59 | this.#ns_id = ns_id; 60 | this.#state = state; 61 | this.#ws = ws; 62 | } 63 | /** @param {string} name */ 64 | get(name) { 65 | return this.#state[name]; 66 | } 67 | /** 68 | * @param {string} key 69 | * @param {any} value 70 | */ 71 | set(key, value) { 72 | this.#state[key] = value; 73 | this.#unsavedKeys.add(key); 74 | this.#target.dispatchEvent( 75 | new CustomEvent(`change:${key}`, { detail: value }), 76 | ); 77 | this.#target.dispatchEvent( 78 | new CustomEvent("change", { detail: value }), 79 | ); 80 | } 81 | /** 82 | * @param {string} name 83 | * @param {Function} callback 84 | */ 85 | on(name, callback) { 86 | this.#target.addEventListener(name, callback); 87 | this.#callbacks.add(name, callback); 88 | } 89 | /** 90 | * @param {string} name 91 | * @param {Function} [callback] 92 | */ 93 | off(name, callback) { 94 | for (const cb of this.#callbacks.remove(name, callback)) { 95 | this.#target.removeEventListener(name, cb); 96 | } 97 | } 98 | /** 99 | * @param {any} msg 100 | * @param {unknown} [callbacks] 101 | * @param {ArrayBuffer[]} [buffers] 102 | */ 103 | send(msg, callbacks, buffers) { 104 | // TODO: impeThrow? 105 | console.error(`model.send is not yet implemented for anyhtmlwidget`); 106 | } 107 | save_changes() { 108 | const unsavedState = Object.fromEntries( 109 | Array.from(this.#unsavedKeys.values()) 110 | .map((key) => [key, this.#state[key]]), 111 | ); 112 | this.#unsavedKeys = new Set(); 113 | if (window && window.Shiny && window.Shiny.setInputValue) { 114 | const eventPrefix = this.#ns_id ? `${this.#ns_id}-` : ""; 115 | Shiny.setInputValue( 116 | `${eventPrefix}anyhtmlwidget_on_save_changes`, 117 | unsavedState, 118 | ); 119 | } else if (this.#ws) { 120 | this.#ws.send(JSON.stringify({ 121 | type: "on_save_changes", 122 | payload: unsavedState, 123 | })); 124 | } 125 | } 126 | } 127 | 128 | function emptyElement(el) { 129 | while (el.firstChild) { 130 | el.removeChild(el.firstChild); 131 | } 132 | } 133 | 134 | HTMLWidgets.widget({ 135 | name: 'anyhtmlwidget', 136 | type: 'output', 137 | factory: function(el, width, height) { 138 | 139 | let widget; 140 | let model; 141 | let cleanup; 142 | let ws; 143 | 144 | return { 145 | renderValue: async function(x) { 146 | if(cleanup && typeof cleanup === "function") { 147 | cleanup(); 148 | cleanup = undefined; 149 | if(ws) { 150 | ws.close(); 151 | ws = undefined; 152 | } 153 | } 154 | // The default can either be an object like { render, initialize } 155 | // or a function that returns this object. 156 | if(!widget) { 157 | const esm = x.esm; 158 | const url = URL.createObjectURL(new Blob([esm], { type: "text/javascript" })); 159 | const mod = await import(/* webpackIgnore: true */ url); 160 | URL.revokeObjectURL(url); 161 | 162 | widget = typeof mod.default === "function" 163 | ? await mod.default() 164 | : mod.default; 165 | 166 | // TODO: initialize here 167 | } 168 | 169 | if(x.port && x.host && !window.Shiny) { 170 | ws = new WebSocket(`ws://${x.host}:${x.port}`); 171 | } 172 | 173 | model = new AnyModel(x.values, x.ns_id, ws); 174 | 175 | if(window && window.Shiny && window.Shiny.addCustomMessageHandler) { 176 | const eventPrefix = x.ns_id ? `${x.ns_id}-` : ''; 177 | Shiny.addCustomMessageHandler(`${eventPrefix}anyhtmlwidget_on_change`, ({ key, value }) => { 178 | model.set(key, value); 179 | }); 180 | } else if(x.port && x.host) { 181 | ws.onmessage = (event) => { 182 | const { type, payload } = JSON.parse(event.data); 183 | if(type === "on_change") { 184 | const { key, value } = payload; 185 | model.set(key, value); 186 | } 187 | }; 188 | } 189 | 190 | try { 191 | emptyElement(el); 192 | // Register cleanup function. 193 | cleanup = await widget.render({ model, el, width, height }); 194 | 195 | } catch(e) { 196 | // TODO: re-throw error 197 | } 198 | }, 199 | resize: async function(width, height) { 200 | // TODO: emit resize event on window (and let user handle)? 201 | if(widget?.resize) { 202 | await widget.resize({ model, el, width, height }); 203 | } 204 | } 205 | }; 206 | } 207 | }); 208 | -------------------------------------------------------------------------------- /inst/htmlwidgets/anyhtmlwidget.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: anyhtmlwidget 3 | version: 0.99.1 4 | src: htmlwidgets/lib 5 | stylesheet: index.css 6 | script: 7 | - index.js 8 | -------------------------------------------------------------------------------- /inst/htmlwidgets/lib/index.css: -------------------------------------------------------------------------------- 1 | html, body, #htmlwidget_container { 2 | height: 100%; 3 | margin: 0; 4 | } 5 | .anyhtmlwidget { 6 | box-sizing: border-box; 7 | } 8 | -------------------------------------------------------------------------------- /inst/htmlwidgets/lib/index.js: -------------------------------------------------------------------------------- 1 | // This is empty 2 | -------------------------------------------------------------------------------- /man/AnyHtmlWidget.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/widget.R 3 | \docType{class} 4 | \name{AnyHtmlWidget} 5 | \alias{AnyHtmlWidget} 6 | \title{AnyHtmlWidget Class} 7 | \value{ 8 | The value. 9 | 10 | The ESM string. 11 | 12 | List of values. 13 | 14 | The width. 15 | 16 | The height. 17 | 18 | The port number. 19 | } 20 | \description{ 21 | Class representing a widget. 22 | } 23 | \details{ 24 | AnyHtmlWidget 25 | } 26 | \section{Methods}{ 27 | \subsection{Public methods}{ 28 | \itemize{ 29 | \item \href{#method-AnyHtmlWidget-new}{\code{AnyHtmlWidget$new()}} 30 | \item \href{#method-AnyHtmlWidget-set_value}{\code{AnyHtmlWidget$set_value()}} 31 | \item \href{#method-AnyHtmlWidget-on_change}{\code{AnyHtmlWidget$on_change()}} 32 | \item \href{#method-AnyHtmlWidget-get_value}{\code{AnyHtmlWidget$get_value()}} 33 | \item \href{#method-AnyHtmlWidget-get_esm}{\code{AnyHtmlWidget$get_esm()}} 34 | \item \href{#method-AnyHtmlWidget-get_values}{\code{AnyHtmlWidget$get_values()}} 35 | \item \href{#method-AnyHtmlWidget-get_width}{\code{AnyHtmlWidget$get_width()}} 36 | \item \href{#method-AnyHtmlWidget-get_height}{\code{AnyHtmlWidget$get_height()}} 37 | \item \href{#method-AnyHtmlWidget-set_values}{\code{AnyHtmlWidget$set_values()}} 38 | \item \href{#method-AnyHtmlWidget-set_mode}{\code{AnyHtmlWidget$set_mode()}} 39 | \item \href{#method-AnyHtmlWidget-start_server}{\code{AnyHtmlWidget$start_server()}} 40 | \item \href{#method-AnyHtmlWidget-stop_server}{\code{AnyHtmlWidget$stop_server()}} 41 | \item \href{#method-AnyHtmlWidget-get_host}{\code{AnyHtmlWidget$get_host()}} 42 | \item \href{#method-AnyHtmlWidget-get_port}{\code{AnyHtmlWidget$get_port()}} 43 | \item \href{#method-AnyHtmlWidget-print}{\code{AnyHtmlWidget$print()}} 44 | \item \href{#method-AnyHtmlWidget-render}{\code{AnyHtmlWidget$render()}} 45 | \item \href{#method-AnyHtmlWidget-clone}{\code{AnyHtmlWidget$clone()}} 46 | } 47 | } 48 | \if{html}{\out{
}} 49 | \if{html}{\out{}} 50 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-new}{}}} 51 | \subsection{Method \code{new()}}{ 52 | Create a new widget instance. 53 | \subsection{Usage}{ 54 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$new(.esm, .mode, .width = NA, .height = NA, .commands = NA, ...)}\if{html}{\out{
}} 55 | } 56 | 57 | \subsection{Arguments}{ 58 | \if{html}{\out{
}} 59 | \describe{ 60 | \item{\code{.esm}}{The EcmaScript module as a string.} 61 | 62 | \item{\code{.mode}}{The widget mode.} 63 | 64 | \item{\code{.width}}{The widget width. Optional.} 65 | 66 | \item{\code{.height}}{The widget height. Optional.} 67 | 68 | \item{\code{.commands}}{TODO} 69 | 70 | \item{\code{...}}{All other named arguments will be used to create active bindings on the instance.} 71 | } 72 | \if{html}{\out{
}} 73 | } 74 | } 75 | \if{html}{\out{
}} 76 | \if{html}{\out{}} 77 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-set_value}{}}} 78 | \subsection{Method \code{set_value()}}{ 79 | Set a value. 80 | \subsection{Usage}{ 81 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$set_value(key, val, emit_change = TRUE)}\if{html}{\out{
}} 82 | } 83 | 84 | \subsection{Arguments}{ 85 | \if{html}{\out{
}} 86 | \describe{ 87 | \item{\code{key}}{The key of the value to set.} 88 | 89 | \item{\code{val}}{The new value.} 90 | 91 | \item{\code{emit_change}}{Should the on_change handler be called?} 92 | } 93 | \if{html}{\out{
}} 94 | } 95 | } 96 | \if{html}{\out{
}} 97 | \if{html}{\out{}} 98 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-on_change}{}}} 99 | \subsection{Method \code{on_change()}}{ 100 | Register a change handler to call if emit_change is TRUE in set_value. 101 | \subsection{Usage}{ 102 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$on_change(callback)}\if{html}{\out{
}} 103 | } 104 | 105 | \subsection{Arguments}{ 106 | \if{html}{\out{
}} 107 | \describe{ 108 | \item{\code{callback}}{A callback function to register.} 109 | } 110 | \if{html}{\out{
}} 111 | } 112 | } 113 | \if{html}{\out{
}} 114 | \if{html}{\out{}} 115 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-get_value}{}}} 116 | \subsection{Method \code{get_value()}}{ 117 | Get a particular value. 118 | \subsection{Usage}{ 119 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$get_value(key)}\if{html}{\out{
}} 120 | } 121 | 122 | \subsection{Arguments}{ 123 | \if{html}{\out{
}} 124 | \describe{ 125 | \item{\code{key}}{The key of the value to get.} 126 | } 127 | \if{html}{\out{
}} 128 | } 129 | } 130 | \if{html}{\out{
}} 131 | \if{html}{\out{}} 132 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-get_esm}{}}} 133 | \subsection{Method \code{get_esm()}}{ 134 | Get the ESM string. 135 | \subsection{Usage}{ 136 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$get_esm()}\if{html}{\out{
}} 137 | } 138 | 139 | } 140 | \if{html}{\out{
}} 141 | \if{html}{\out{}} 142 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-get_values}{}}} 143 | \subsection{Method \code{get_values()}}{ 144 | Get all widget values 145 | \subsection{Usage}{ 146 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$get_values()}\if{html}{\out{
}} 147 | } 148 | 149 | } 150 | \if{html}{\out{
}} 151 | \if{html}{\out{}} 152 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-get_width}{}}} 153 | \subsection{Method \code{get_width()}}{ 154 | Get the widget width. 155 | \subsection{Usage}{ 156 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$get_width()}\if{html}{\out{
}} 157 | } 158 | 159 | } 160 | \if{html}{\out{
}} 161 | \if{html}{\out{}} 162 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-get_height}{}}} 163 | \subsection{Method \code{get_height()}}{ 164 | Get the widget height. 165 | \subsection{Usage}{ 166 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$get_height()}\if{html}{\out{
}} 167 | } 168 | 169 | } 170 | \if{html}{\out{
}} 171 | \if{html}{\out{}} 172 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-set_values}{}}} 173 | \subsection{Method \code{set_values()}}{ 174 | Set all values. TODO: is this ever used? 175 | \subsection{Usage}{ 176 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$set_values(new_values)}\if{html}{\out{
}} 177 | } 178 | 179 | \subsection{Arguments}{ 180 | \if{html}{\out{
}} 181 | \describe{ 182 | \item{\code{new_values}}{A list of new values.} 183 | } 184 | \if{html}{\out{
}} 185 | } 186 | } 187 | \if{html}{\out{
}} 188 | \if{html}{\out{}} 189 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-set_mode}{}}} 190 | \subsection{Method \code{set_mode()}}{ 191 | Set the widget mode. 192 | \subsection{Usage}{ 193 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$set_mode(mode)}\if{html}{\out{
}} 194 | } 195 | 196 | \subsection{Arguments}{ 197 | \if{html}{\out{
}} 198 | \describe{ 199 | \item{\code{mode}}{The new widget mode.} 200 | } 201 | \if{html}{\out{
}} 202 | } 203 | } 204 | \if{html}{\out{
}} 205 | \if{html}{\out{}} 206 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-start_server}{}}} 207 | \subsection{Method \code{start_server()}}{ 208 | Start the server, if not running. 209 | \subsection{Usage}{ 210 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$start_server()}\if{html}{\out{
}} 211 | } 212 | 213 | } 214 | \if{html}{\out{
}} 215 | \if{html}{\out{}} 216 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-stop_server}{}}} 217 | \subsection{Method \code{stop_server()}}{ 218 | Stop the server, if running. 219 | \subsection{Usage}{ 220 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$stop_server()}\if{html}{\out{
}} 221 | } 222 | 223 | } 224 | \if{html}{\out{
}} 225 | \if{html}{\out{}} 226 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-get_host}{}}} 227 | \subsection{Method \code{get_host()}}{ 228 | Get the server hostname. 229 | \subsection{Usage}{ 230 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$get_host()}\if{html}{\out{
}} 231 | } 232 | 233 | \subsection{Returns}{ 234 | The hostname as a string. 235 | } 236 | } 237 | \if{html}{\out{
}} 238 | \if{html}{\out{}} 239 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-get_port}{}}} 240 | \subsection{Method \code{get_port()}}{ 241 | Get the server port. 242 | \subsection{Usage}{ 243 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$get_port()}\if{html}{\out{
}} 244 | } 245 | 246 | } 247 | \if{html}{\out{
}} 248 | \if{html}{\out{}} 249 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-print}{}}} 250 | \subsection{Method \code{print()}}{ 251 | Custom print function for the R6 class. 252 | If mode is "shiny", falls back to original R6 print behavior. 253 | Otherwise, renders the widget. 254 | \subsection{Usage}{ 255 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$print()}\if{html}{\out{
}} 256 | } 257 | 258 | } 259 | \if{html}{\out{
}} 260 | \if{html}{\out{}} 261 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-render}{}}} 262 | \subsection{Method \code{render()}}{ 263 | Render the widget. 264 | \subsection{Usage}{ 265 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$render()}\if{html}{\out{
}} 266 | } 267 | 268 | } 269 | \if{html}{\out{
}} 270 | \if{html}{\out{}} 271 | \if{latex}{\out{\hypertarget{method-AnyHtmlWidget-clone}{}}} 272 | \subsection{Method \code{clone()}}{ 273 | The objects of this class are cloneable with this method. 274 | \subsection{Usage}{ 275 | \if{html}{\out{
}}\preformatted{AnyHtmlWidget$clone(deep = FALSE)}\if{html}{\out{
}} 276 | } 277 | 278 | \subsection{Arguments}{ 279 | \if{html}{\out{
}} 280 | \describe{ 281 | \item{\code{deep}}{Whether to make a deep clone.} 282 | } 283 | \if{html}{\out{
}} 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /man/anyhtmlwidget_output.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/widget.R 3 | \name{anyhtmlwidget_output} 4 | \alias{anyhtmlwidget_output} 5 | \title{Internal Shiny UI binding for anyhtmlwidget.} 6 | \usage{ 7 | anyhtmlwidget_output(output_id, width = "100\%", height = "400px") 8 | } 9 | \arguments{ 10 | \item{output_id}{output variable to read from} 11 | 12 | \item{width, height}{Must be a valid CSS unit (like \code{'100\%'}, 13 | \code{'400px'}, \code{'auto'}) or a number, which will be coerced to a 14 | string and have \code{'px'} appended.} 15 | 16 | \item{expr}{An expression that generates a vitessce} 17 | 18 | \item{env}{The environment in which to evaluate \code{expr}.} 19 | 20 | \item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This 21 | is useful if you want to save an expression in a variable.} 22 | } 23 | \value{ 24 | The Shiny UI element. 25 | } 26 | \description{ 27 | Internal Shiny UI binding for anyhtmlwidget. 28 | } 29 | \keyword{internal} 30 | -------------------------------------------------------------------------------- /man/render_anyhtmlwidget.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/widget.R 3 | \name{render_anyhtmlwidget} 4 | \alias{render_anyhtmlwidget} 5 | \title{Internal Shiny server binding for anyhtmlwidget.} 6 | \usage{ 7 | render_anyhtmlwidget(expr, env = parent.frame(), quoted = FALSE) 8 | } 9 | \description{ 10 | Internal Shiny server binding for anyhtmlwidget. 11 | } 12 | \keyword{internal} 13 | -------------------------------------------------------------------------------- /man/the_anyhtmlwidget.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/widget.R 3 | \name{the_anyhtmlwidget} 4 | \alias{the_anyhtmlwidget} 5 | \title{The internal function that creates the htmlwidget.} 6 | \usage{ 7 | the_anyhtmlwidget( 8 | esm, 9 | values = NULL, 10 | ns_id = NULL, 11 | width = NULL, 12 | height = NULL, 13 | port = NULL, 14 | host = NULL, 15 | element_id = NULL 16 | ) 17 | } 18 | \arguments{ 19 | \item{esm}{The ES Module as a string.} 20 | 21 | \item{values}{The values that will be used for the initial Model state.} 22 | 23 | \item{ns_id}{Namespace ID, only used when in the Shiny module mode. Optional.} 24 | 25 | \item{width}{The width of the widget as a number or CSS string. Optional.} 26 | 27 | \item{height}{The height of the widget as a number or CSS string. Optional.} 28 | 29 | \item{port}{The port of the WebSocket server, when in dynamic mode. Optional.} 30 | 31 | \item{host}{The host of the WebSocket server, when in dynamic mode. Optional.} 32 | 33 | \item{element_id}{An element ID. Optional.} 34 | } 35 | \value{ 36 | The result of htmlwidgets::createWidget. 37 | } 38 | \description{ 39 | The internal function that creates the htmlwidget. 40 | } 41 | \keyword{internal} 42 | -------------------------------------------------------------------------------- /man/widgetServer.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/widget.R 3 | \name{widgetServer} 4 | \alias{widgetServer} 5 | \title{Shiny module server for anyhtmlwidgets.} 6 | \usage{ 7 | widgetServer(id, w) 8 | } 9 | \arguments{ 10 | \item{id}{The matching output ID used in the Shiny UI.} 11 | 12 | \item{w}{The widget instance.} 13 | } 14 | \value{ 15 | reactiveValues corresponding to the widget's active bindings. 16 | } 17 | \description{ 18 | Shiny module server for anyhtmlwidgets. 19 | } 20 | -------------------------------------------------------------------------------- /man/widgetUI.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/widget.R 3 | \name{widgetUI} 4 | \alias{widgetUI} 5 | \title{Shiny module UI for anyhtmlwidgets.} 6 | \usage{ 7 | widgetUI(id, width = "100\%", height = "400px") 8 | } 9 | \arguments{ 10 | \item{id}{The output ID.} 11 | 12 | \item{width}{The widget width. Optional.} 13 | 14 | \item{height}{The widget height. Optional.} 15 | } 16 | \description{ 17 | Shiny module UI for anyhtmlwidgets. 18 | } 19 | -------------------------------------------------------------------------------- /pkgdown/_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://keller-mark.github.io/anyhtmlwidget/ 2 | 3 | template: 4 | bootstrap: 5 5 | bslib: 6 | base_font: {google: "Inter"} 7 | 8 | authors: 9 | Mark Keller: 10 | href: https://github.com/keller-mark 11 | 12 | navbar: 13 | structure: 14 | left: [home, reference, articles] 15 | right: [github] 16 | 17 | reference: 18 | - title: "Core" 19 | desc: > 20 | Use widgets in RStudio Viewer pane. 21 | - contents: 22 | - AnyHtmlWidget 23 | - title: "Shiny" 24 | desc: > 25 | Use widgets in Shiny applications. 26 | - contents: 27 | - widgetUI 28 | - widgetServer 29 | 30 | 31 | articles: 32 | - title: Articles 33 | navbar: Tutorials 34 | contents: 35 | - basics 36 | - shiny 37 | - title: Articles 38 | navbar: Troubleshooting 39 | contents: 40 | - session_info 41 | -------------------------------------------------------------------------------- /vignettes/basics.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Basics" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Basics} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | 11 | ```{r, include = FALSE} 12 | knitr::opts_chunk$set( 13 | root.dir = dirname(dirname(getwd())), 14 | rmarkdown.pandoc.to = knitr::opts_knit$get("rmarkdown.pandoc.to"), 15 | collapse = TRUE, 16 | comment = "#>", 17 | out.width = "100%" 18 | ) 19 | ``` 20 | 21 | ## Basics 22 | 23 | ```{r} 24 | library(anyhtmlwidget) 25 | 26 | esm <- " 27 | function render({ el, model }) { 28 | el.style.border = '4px solid red'; 29 | let count = () => model.get('count'); 30 | let btn = document.createElement('button'); 31 | btn.innerHTML = `count is ${count()}`; 32 | btn.addEventListener('click', () => { 33 | model.set('count', count() + 1); 34 | model.save_changes(); 35 | }); 36 | model.on('change:count', () => { 37 | btn.innerHTML = `count is ${count()}`; 38 | }); 39 | el.appendChild(btn); 40 | } 41 | export default { render }; 42 | " 43 | 44 | widget <- AnyHtmlWidget$new( 45 | .esm = esm, 46 | .mode = "static", 47 | .height='400px', 48 | count = 1 49 | ) 50 | ``` 51 | 52 | ```{r, eval = FALSE, echo = TRUE} 53 | widget 54 | ``` 55 | 56 | ```{r, eval = TRUE, echo = FALSE} 57 | if(!is.null(knitr::opts_knit$get("rmarkdown.pandoc.to")) && knitr::opts_knit$get("rmarkdown.pandoc.to") == "html") { 58 | tryCatch({ 59 | pkgdown::pkgdown_print(widget$render()) 60 | }) 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /vignettes/session_info.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "R Session Info" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{R Session Info} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | This page runs the `sessionInfo()` function and prints the results. 11 | The output can be used to check the dependency versions that were used to run tests. 12 | 13 | ```{r include=TRUE, echo=TRUE} 14 | library(anyhtmlwidget) 15 | sessionInfo() 16 | ``` -------------------------------------------------------------------------------- /vignettes/shiny.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Usage in Shiny" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Usage in Shiny} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | out.width = "100%" 15 | ) 16 | ``` 17 | 18 | ## Usage in Shiny 19 | 20 | ```{r} 21 | library(shiny) 22 | library(anyhtmlwidget) 23 | 24 | esm <- " 25 | function render({ el, model }) { 26 | let count = () => model.get('count'); 27 | el.style.border = '4px solid red'; 28 | let btn = document.createElement('button'); 29 | btn.innerHTML = `count button ${count()}`; 30 | btn.addEventListener('click', () => { 31 | model.set('count', count() + 1); 32 | model.save_changes(); 33 | }); 34 | model.on('change:count', () => { 35 | btn.innerHTML = `count is ${count()}`; 36 | }); 37 | el.appendChild(btn); 38 | } 39 | export default { render }; 40 | " 41 | 42 | widget <- AnyHtmlWidget$new(.esm = esm, .mode = "shiny", count = 2) 43 | 44 | 45 | ui <- fluidPage( 46 | "anyhtmlwidget", 47 | widgetUI("my_widget"), 48 | verbatimTextOutput("values"), 49 | actionButton("go", label = "Go") 50 | ) 51 | 52 | server <- function(input, output, session) { 53 | rv <- widgetServer("my_widget", widget) 54 | 55 | output$values <- renderPrint({ 56 | rv$count 57 | }) 58 | 59 | observeEvent(input$go, { 60 | rv$count <- 999 61 | }) 62 | } 63 | 64 | shinyApp(ui, server) 65 | ``` 66 | --------------------------------------------------------------------------------