├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ └── R-CMD-check.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── funs.R └── logs.R ├── README.Rmd ├── README.md ├── cran-comments.md ├── gargoyle.Rproj ├── inst └── example │ └── examples.R ├── man ├── Event.Rd ├── logs.Rd └── on.Rd ├── tests ├── testthat.R └── testthat │ ├── test-funs.R │ └── test-logs.R └── vignettes ├── .gitignore └── gargoyle.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^gargoyle\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^CODE_OF_CONDUCT\.md$ 6 | ^\.github$ 7 | ^cran-comments\.md$ 8 | ^README\.html$ 9 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macos-latest, r: 'release'} 22 | - {os: windows-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 24 | - {os: ubuntu-latest, r: 'release'} 25 | - {os: ubuntu-latest, r: 'oldrel-1'} 26 | 27 | env: 28 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 29 | R_KEEP_PKG_SOURCE: yes 30 | 31 | steps: 32 | - uses: actions/checkout@v3 33 | 34 | - uses: r-lib/actions/setup-pandoc@v2 35 | 36 | - uses: r-lib/actions/setup-r@v2 37 | with: 38 | r-version: ${{ matrix.config.r }} 39 | http-user-agent: ${{ matrix.config.http-user-agent }} 40 | use-public-rspm: true 41 | 42 | - uses: r-lib/actions/setup-r-dependencies@v2 43 | with: 44 | extra-packages: any::rcmdcheck 45 | needs: check 46 | 47 | - uses: r-lib/actions/check-r-package@v2 48 | with: 49 | upload-snapshots: true 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | inst/doc 3 | .Rhistory 4 | .Rdata 5 | .httr-oauth 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who 4 | contribute through reporting issues, posting feature requests, updating documentation, 5 | submitting pull requests or patches, and other activities. 6 | 7 | We are committed to making participation in this project a harassment-free experience for 8 | everyone, regardless of level of experience, gender, gender identity and expression, 9 | sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 10 | 11 | Examples of unacceptable behavior by participants include the use of sexual language or 12 | imagery, derogatory comments or personal attacks, trolling, public or private harassment, 13 | insults, or other unprofessional conduct. 14 | 15 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 16 | commits, code, wiki edits, issues, and other contributions that are not aligned to this 17 | Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed 18 | from the project team. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 21 | opening an issue or contacting one or more of the project maintainers. 22 | 23 | This Code of Conduct is adapted from the Contributor Covenant 24 | (https://www.contributor-covenant.org), version 1.0.0, available at 25 | https://contributor-covenant.org/version/1/0/0/. 26 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: gargoyle 2 | Title: An Event-Based Mechanism for 'Shiny' 3 | Version: 0.0.1 4 | Authors@R: person("Colin", "Fay", email = "contact@colinfay.me", role = c("aut", "cre")) 5 | Description: An event-Based framework for building 'Shiny' apps. 6 | Instead of relying on standard 'Shiny' reactive objects, this 7 | package allow to relying on a lighter set of triggers, so that 8 | reactive contexts can be invalidated with more control. 9 | License: MIT + file LICENSE 10 | URL: https://github.com/ColinFay/gargoyle 11 | BugReports: https://github.com/ColinFay/gargoyle/issues 12 | Encoding: UTF-8 13 | LazyData: true 14 | RoxygenNote: 7.2.3 15 | Imports: 16 | shiny, 17 | attempt 18 | Suggests: 19 | knitr, 20 | rmarkdown, 21 | testthat (>= 3.0.0) 22 | VignetteBuilder: knitr 23 | Config/testthat/edition: 3 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2019 2 | COPYRIGHT HOLDER: Colin Fay 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 Colin Fay 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(clear_gargoyle_logs) 4 | export(get_gargoyle_logs) 5 | export(init) 6 | export(on) 7 | export(trigger) 8 | export(watch) 9 | importFrom(attempt,stop_if) 10 | importFrom(shiny,getDefaultReactiveDomain) 11 | importFrom(shiny,observeEvent) 12 | importFrom(shiny,reactiveVal) 13 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # gargoyle 0.0.0.9000 2 | 3 | * Added a `NEWS.md` file to track changes to the package. 4 | -------------------------------------------------------------------------------- /R/funs.R: -------------------------------------------------------------------------------- 1 | #' Initiate, trigger, event 2 | #' 3 | #' @param name,... The name(s) of the events 4 | #' @param session The shiny session object 5 | #' 6 | #' @rdname Event 7 | #' 8 | #' @return The `session` object invisibly. 9 | #' These functions are mainly used for side-effects. 10 | #' 11 | #' @importFrom shiny reactiveVal 12 | #' @export 13 | #' @examples 14 | #' if (interactive()){ 15 | #' library(shiny) 16 | #' library(gargoyle) 17 | #' options("gargoyle.talkative" = TRUE) 18 | #' ui <- function(request){ 19 | #' tagList( 20 | #' h4('Go'), 21 | #' actionButton("y", "y"), 22 | #' h4('Output of z$v'), 23 | #' tableOutput("evt") 24 | #' ) 25 | #' } 26 | #' 27 | #' server <- function(input, output, session){ 28 | #' 29 | #' # Initiating the flags 30 | #' init("airquality", "iris", "renderiris") 31 | #' 32 | #' # Creating a new env to store values, instead of 33 | #' # a reactive structure 34 | #' z <- new.env() 35 | #' 36 | #' observeEvent( input$y , { 37 | #' z$v <- mtcars 38 | #' # Triggering the flag 39 | #' trigger("airquality") 40 | #' }) 41 | #' 42 | #' on("airquality", { 43 | #' # Triggering the flag 44 | #' z$v <- airquality 45 | #' trigger("iris") 46 | #' }) 47 | #' 48 | #' on("iris", { 49 | #' # Triggering the flag 50 | #' z$v <- iris 51 | #' trigger("renderiris") 52 | #' }) 53 | #' 54 | #' output$evt <- renderTable({ 55 | #' # This part will only render when the renderiris 56 | #' # flag is triggered 57 | #' watch("renderiris") 58 | #' head(z$v) 59 | #' }) 60 | #' 61 | #' } 62 | #' 63 | #' shinyApp(ui, server) 64 | #' 65 | #' } 66 | init <- function(..., session = getDefaultReactiveDomain()){ 67 | lapply( 68 | list(...), 69 | function(x){ 70 | session$userData[[x]] <- reactiveVal(0) 71 | } 72 | ) 73 | 74 | } 75 | 76 | #' @rdname Event 77 | #' @export 78 | trigger <- function(..., session = getDefaultReactiveDomain()){ 79 | .logs$log <- rbind( 80 | .logs$log, 81 | data.frame( 82 | what = c(...), 83 | time = as.character(Sys.time()), 84 | stringsAsFactors = FALSE 85 | ) 86 | ) 87 | lapply( 88 | list(...), 89 | function(x){ 90 | if (getOption("gargoyle.talkative", FALSE)){ 91 | cat( 92 | "- [Gargoyle] Triggering", 93 | x, 94 | "\n" 95 | ) 96 | } 97 | session$userData[[x]]( 98 | session$userData[[x]]() + 1 99 | ) 100 | } 101 | ) 102 | 103 | } 104 | #' @rdname Event 105 | #' @export 106 | watch <- function(name, session = getDefaultReactiveDomain()){ 107 | session$userData[[name]]() 108 | } 109 | 110 | 111 | #' React on an event 112 | #' 113 | #' @param name the name of the event to react to 114 | #' @param expr the expression to run when the event 115 | #' is triggered. 116 | #' @param session The shiny session object 117 | #' 118 | #' @return An observeEvent object. This object will 119 | #' rarely be used, `on` is mainly called for side-effects. 120 | #' 121 | #' @export 122 | #' @importFrom shiny observeEvent getDefaultReactiveDomain 123 | #' @importFrom attempt stop_if 124 | on <- function( 125 | name, 126 | expr, 127 | session = getDefaultReactiveDomain() 128 | ){ 129 | 130 | stop_if( 131 | session$userData[[name]], 132 | is.null, 133 | sprintf( 134 | "[Gargoyle] Flag %s hasn't been initiated: can't listen to it.", 135 | name 136 | ) 137 | ) 138 | observeEvent( 139 | substitute(gargoyle::watch(name, session = session)), 140 | { 141 | substitute(expr) 142 | }, 143 | event.quoted = TRUE, 144 | handler.quoted = TRUE, 145 | ignoreInit = TRUE, 146 | event.env = parent.frame(), 147 | handler.env = parent.frame() 148 | ) 149 | } 150 | -------------------------------------------------------------------------------- /R/logs.R: -------------------------------------------------------------------------------- 1 | .logs <- new.env() 2 | .logs$log <- data.frame( 3 | what = character(0), 4 | time = character(0), 5 | stringsAsFactors = FALSE 6 | ) 7 | 8 | #' Handle logs 9 | #' 10 | #' Get / Clear the logs of all the time the `trigger()` functions are launched. 11 | #' 12 | #' @return A data.frame of the logs. 13 | #' @export 14 | #' @rdname logs 15 | #' @examples 16 | #' if (interactive()){ 17 | #' get_gargoyle_logs() 18 | #' clear_gargoyle_logs() 19 | #' } 20 | get_gargoyle_logs <- function(){ 21 | return(.logs$log) 22 | } 23 | #' @export 24 | #' @rdname logs 25 | clear_gargoyle_logs <- function(){ 26 | .logs$log <- data.frame( 27 | what = character(0), 28 | time = character(0), 29 | stringsAsFactors = FALSE 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | # gargoyle 16 | 17 | 18 | [![Lifecycle: stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) 19 | [![R-CMD-check](https://github.com/ColinFay/gargoyle/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/ColinFay/gargoyle/actions/workflows/R-CMD-check.yaml) 20 | 21 | 22 | The goal of gargoyle is to provide an event-based mechanism for `{shiny}`. 23 | 24 | ## Installation 25 | 26 | You can install the dev version of `{gargoyle}` with: 27 | 28 | ``` r 29 | remotes::install_github("ColinFay/gargoyle") 30 | ``` 31 | 32 | ## About 33 | 34 | You're reading the doc about version : `r pkgload::pkg_version()` 35 | 36 | This README has been compiled on the 37 | 38 | ```{r} 39 | Sys.time() 40 | ``` 41 | 42 | Here are the test & coverage results : 43 | 44 | ```{r} 45 | devtools::check(quiet = TRUE) 46 | ``` 47 | 48 | ```{r echo = FALSE} 49 | unloadNamespace("shinipsum") 50 | ``` 51 | 52 | ```{r} 53 | covr::package_coverage() 54 | ``` 55 | 56 | ### What the heck? 57 | 58 | `{gargoyle}` is a package that provides wrappers around `{shiny}` to turn your app into and event-based application instead of a full reactive app. 59 | The framework is centered around a `watch` & `trigger` mechanism. 60 | 61 | It works with classical UI, and just needs tweaking the server side of your app. 62 | 63 | `{shiny}`'s default reactive behavior is very helpful when it comes to building small applications. 64 | Because, you know, the good thing about reactivity is that when something moves somewhere, it's updated everywhere. 65 | But the bad thing about reactivity is that when something moves somewhere, it's updated everywhere. 66 | So it does work pretty well on small apps, but can get very complicated on bigger apps, and can quickly get out of hands. 67 | 68 | That's where `{gargoyle}` comes into play: it provides an event based paradigm for building your apps, so that reactivity happens under a controlled flow. 69 | 70 | ### For whom? 71 | 72 | If you're just building small `{shiny}` apps you're probably good with `{shiny}` default reactive behavior. 73 | But if ever you've struggled with reactivity in larger apps you might find `{gargoyle}` useful: only invalidate contexts when you want and make the general flow of your app more predictable! 74 | 75 | ### The trade-off 76 | 77 | `{gargoyle}` will be more verbose and will demand more work upfront to make things happen. 78 | I believe this is for the best if you're working on a big project: from the very beginning 79 | it forces the user to think more carefully about app design and the reactive flow. 80 | 81 | ## Design pattern 82 | 83 | `{gargoyle}` has: 84 | 85 | + `init`, `watch` & `trigger`, which allow to initiate, watch, and trigger an event 86 | 87 | + `on`, that runs the `expr` when the event is triggered 88 | 89 | `gargoyle::trigger()` can print messages to the console using `options("gargoyle.talkative" = TRUE)`. 90 | 91 | ## Example 92 | 93 | ```{r eval = FALSE} 94 | library(shiny) 95 | library(gargoyle) 96 | options("gargoyle.talkative" = TRUE) 97 | ui <- function(request){ 98 | tagList( 99 | h4('Go'), 100 | actionButton("y", "y"), 101 | h4('Output of z$v'), 102 | tableOutput("evt") 103 | ) 104 | } 105 | 106 | server <- function(input, output, session){ 107 | 108 | # Initiating the flags 109 | init("airquality", "iris", "renderiris") 110 | 111 | # Creating a new env to store values, instead of 112 | # a reactive structure 113 | z <- new.env() 114 | 115 | observeEvent(input$y , { 116 | z$v <- mtcars 117 | # Triggering the flag 118 | trigger("airquality") 119 | }) 120 | 121 | on("airquality", { 122 | # Triggering the flag 123 | z$v <- airquality 124 | trigger("iris") 125 | }) 126 | 127 | on("iris", { 128 | # Triggering the flag 129 | z$v <- iris 130 | trigger("renderiris") 131 | }) 132 | 133 | output$evt <- renderTable({ 134 | # This part will only render when the renderiris 135 | # flag is triggered 136 | watch("renderiris") 137 | head(z$v) 138 | }) 139 | 140 | } 141 | 142 | shinyApp(ui, server) 143 | ``` 144 | 145 | You can then get & clear the logs of the times the triggers were called: 146 | 147 | ```{r eval = FALSE} 148 | get_gargoyle_logs() 149 | clear_gargoyle_logs() 150 | ``` 151 | 152 | 153 |
154 | 155 | ## Code of Conduct 156 | 157 | Please note that the gargoyle project is released with a [Contributor Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/). By contributing to this project, you agree to abide by its terms. 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # gargoyle 5 | 6 | 7 | 8 | [![Lifecycle: 9 | stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) 10 | [![R-CMD-check](https://github.com/ColinFay/gargoyle/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/ColinFay/gargoyle/actions/workflows/R-CMD-check.yaml) 11 | 12 | 13 | The goal of gargoyle is to provide an event-based mechanism for 14 | `{shiny}`. 15 | 16 | ## Installation 17 | 18 | You can install the dev version of `{gargoyle}` with: 19 | 20 | ``` r 21 | remotes::install_github("ColinFay/gargoyle") 22 | ``` 23 | 24 | ## About 25 | 26 | You’re reading the doc about version : 0.0.1 27 | 28 | This README has been compiled on the 29 | 30 | ``` r 31 | Sys.time() 32 | #> [1] "2023-03-27 16:53:20 CEST" 33 | ``` 34 | 35 | Here are the test & coverage results : 36 | 37 | ``` r 38 | devtools::check(quiet = TRUE) 39 | #> ℹ Loading gargoyle 40 | #> ── R CMD check results ───────────────────────────────────── gargoyle 0.0.1 ──── 41 | #> Duration: 10.3s 42 | #> 43 | #> 0 errors ✔ | 0 warnings ✔ | 0 notes ✔ 44 | ``` 45 | 46 | ``` r 47 | covr::package_coverage() 48 | #> gargoyle Coverage: 56.36% 49 | #> R/funs.R: 51.02% 50 | #> R/logs.R: 100.00% 51 | ``` 52 | 53 | ### What the heck? 54 | 55 | `{gargoyle}` is a package that provides wrappers around `{shiny}` to 56 | turn your app into and event-based application instead of a full 57 | reactive app. The framework is centered around a `listen` & `trigger` 58 | mechanism. 59 | 60 | It works with classical UI, and just needs tweaking the server side of 61 | your app. 62 | 63 | `{shiny}`’s default reactive behavior is very helpful when it comes to 64 | building small applications. Because, you know, the good thing about 65 | reactivity is that when something moves somewhere, it’s updated 66 | everywhere. But the bad thing about reactivity is that when something 67 | moves somewhere, it’s updated everywhere. So it does work pretty well on 68 | small apps, but can get very complicated on bigger apps, and can quickly 69 | get out of hands. 70 | 71 | That’s where `{gargoyle}` comes into play: it provides an event based 72 | paradigm for building your apps, so that things happen under a control 73 | flow. 74 | 75 | ### For whom? 76 | 77 | If you’re just building small `{shiny}` apps, you’re probably good with 78 | `{shiny}` default reactive behavior. But if ever you’ve struggled with 79 | reactivity on more bigger apps, you might find `{gargoyle}` useful. 80 | 81 | ### The trade-off 82 | 83 | `{gargoyle}` will be more verbose and will demand more work upfront to 84 | make things happen. I believe this is for the best if you’re working on 85 | a big project. 86 | 87 | ## Design pattern 88 | 89 | `{gargoyle}` has: 90 | 91 | - `init`, `listen` & `trigger`, which allow to initiate, listen on, and 92 | trigger an event 93 | 94 | - `on`, that runs the `expr` when the event in triggered 95 | 96 | `gargoyle::trigger()` can print messages to the console using 97 | `options("gargoyle.talkative" = TRUE)`. 98 | 99 | ## Example 100 | 101 | ``` r 102 | library(shiny) 103 | library(gargoyle) 104 | options("gargoyle.talkative" = TRUE) 105 | ui <- function(request){ 106 | tagList( 107 | h4('Go'), 108 | actionButton("y", "y"), 109 | h4('Output of z$v'), 110 | tableOutput("evt") 111 | ) 112 | } 113 | 114 | server <- function(input, output, session){ 115 | 116 | # Initiating the flags 117 | init("airquality", "iris", "renderiris") 118 | 119 | # Creating a new env to store values, instead of 120 | # a reactive structure 121 | z <- new.env() 122 | 123 | observeEvent( input$y , { 124 | z$v <- mtcars 125 | # Triggering the flag 126 | trigger("airquality") 127 | }) 128 | 129 | on("airquality", { 130 | # Triggering the flag 131 | z$v <- airquality 132 | trigger("iris") 133 | }) 134 | 135 | on("iris", { 136 | # Triggering the flag 137 | z$v <- iris 138 | trigger("renderiris") 139 | }) 140 | 141 | output$evt <- renderTable({ 142 | # This part will only render when the renderiris 143 | # flag is triggered 144 | watch("renderiris") 145 | head(z$v) 146 | }) 147 | 148 | } 149 | 150 | shinyApp(ui, server) 151 | ``` 152 | 153 | You can then get & clear the logs of the times the triggers were called: 154 | 155 | ``` r 156 | get_gargoyle_logs() 157 | clear_gargoyle_logs() 158 | ``` 159 | 160 |
161 | 162 | ## Code of Conduct 163 | 164 | Please note that the gargoyle project is released with a [Contributor 165 | Code of 166 | Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/). 167 | By contributing to this project, you agree to abide by its terms. 168 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | * local R installation, R 3.6.1 3 | * ubuntu 16.04 (on travis-ci), R 3.6.1 4 | * win-builder (devel) 5 | 6 | ## R CMD check results 7 | 8 | 0 errors | 0 warnings | 1 note 9 | 10 | This is a resubmission after CRAN feedback 11 | 12 | > Please add \value to .Rd files regarding exported methods and explain 13 | > the functions results in the documentation. Please write about the 14 | > structure of the output (class) and also what the output means. (If a 15 | > function does not return a value, please document that too, e.g. 16 | > \value{No return value, called for side effects} or similar) 17 | > Missing Rd-tags: 18 | > Event.Rd: \value 19 | > on.Rd: \value 20 | 21 | 22 | => Added `\value` 23 | 24 | => Checked with `urlchecker::url_check()` 25 | -------------------------------------------------------------------------------- /gargoyle.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 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 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /inst/example/examples.R: -------------------------------------------------------------------------------- 1 | ui <- function(request){ 2 | tagList( 3 | h4('Go'), 4 | actionButton("y", "y"), 5 | h4('Output of z$v'), 6 | tableOutput("evt") 7 | ) 8 | } 9 | 10 | server <- function(input, output, session){ 11 | 12 | # Initiating the flags 13 | init("airquality", "iris", "renderiris") 14 | 15 | # Creating a new env to store values, instead of 16 | # a reactive structure 17 | z <- new.env() 18 | 19 | observeEvent( input$y , { 20 | z$v <- mtcars 21 | # Triggering the flag 22 | trigger("airquality") 23 | }) 24 | 25 | on("airquality", { 26 | # Triggering the flag 27 | z$v <- airquality 28 | trigger("iris") 29 | }) 30 | 31 | on("iris", { 32 | # Triggering the flag 33 | z$v <- iris 34 | trigger("renderiris") 35 | }) 36 | 37 | output$evt <- renderTable({ 38 | # This part will only render when the renderiris 39 | # flag is triggered 40 | watch("renderiris") 41 | head(z$v) 42 | }) 43 | 44 | } 45 | 46 | shinyApp(ui, server) 47 | -------------------------------------------------------------------------------- /man/Event.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/funs.R 3 | \name{init} 4 | \alias{init} 5 | \alias{trigger} 6 | \alias{watch} 7 | \title{Initiate, trigger, event} 8 | \usage{ 9 | init(..., session = getDefaultReactiveDomain()) 10 | 11 | trigger(..., session = getDefaultReactiveDomain()) 12 | 13 | watch(name, session = getDefaultReactiveDomain()) 14 | } 15 | \arguments{ 16 | \item{session}{The shiny session object} 17 | 18 | \item{name, ...}{The name(s) of the events} 19 | } 20 | \value{ 21 | The `session` object invisibly. 22 | These functions are mainly used for side-effects. 23 | } 24 | \description{ 25 | Initiate, trigger, event 26 | } 27 | \examples{ 28 | if (interactive()){ 29 | library(shiny) 30 | library(gargoyle) 31 | options("gargoyle.talkative" = TRUE) 32 | ui <- function(request){ 33 | tagList( 34 | h4('Go'), 35 | actionButton("y", "y"), 36 | h4('Output of z$v'), 37 | tableOutput("evt") 38 | ) 39 | } 40 | 41 | server <- function(input, output, session){ 42 | 43 | # Initiating the flags 44 | init("airquality", "iris", "renderiris") 45 | 46 | # Creating a new env to store values, instead of 47 | # a reactive structure 48 | z <- new.env() 49 | 50 | observeEvent( input$y , { 51 | z$v <- mtcars 52 | # Triggering the flag 53 | trigger("airquality") 54 | }) 55 | 56 | on("airquality", { 57 | # Triggering the flag 58 | z$v <- airquality 59 | trigger("iris") 60 | }) 61 | 62 | on("iris", { 63 | # Triggering the flag 64 | z$v <- iris 65 | trigger("renderiris") 66 | }) 67 | 68 | output$evt <- renderTable({ 69 | # This part will only render when the renderiris 70 | # flag is triggered 71 | watch("renderiris") 72 | head(z$v) 73 | }) 74 | 75 | } 76 | 77 | shinyApp(ui, server) 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /man/logs.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/logs.R 3 | \name{get_gargoyle_logs} 4 | \alias{get_gargoyle_logs} 5 | \alias{clear_gargoyle_logs} 6 | \title{Handle logs} 7 | \usage{ 8 | get_gargoyle_logs() 9 | 10 | clear_gargoyle_logs() 11 | } 12 | \value{ 13 | A data.frame of the logs. 14 | } 15 | \description{ 16 | Get / Clear the logs of all the time the `trigger()` functions are launched. 17 | } 18 | \examples{ 19 | if (interactive()){ 20 | get_gargoyle_logs() 21 | clear_gargoyle_logs() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /man/on.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/funs.R 3 | \name{on} 4 | \alias{on} 5 | \title{React on an event} 6 | \usage{ 7 | on(name, expr, session = getDefaultReactiveDomain()) 8 | } 9 | \arguments{ 10 | \item{name}{the name of the event to react to} 11 | 12 | \item{expr}{the expression to run when the event 13 | is triggered.} 14 | 15 | \item{session}{The shiny session object} 16 | } 17 | \value{ 18 | An observeEvent object. This object will 19 | rarely be used, `on` is mainly called for side-effects. 20 | } 21 | \description{ 22 | React on an event 23 | } 24 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(gargoyle) 3 | 4 | test_check("gargoyle") 5 | -------------------------------------------------------------------------------- /tests/testthat/test-funs.R: -------------------------------------------------------------------------------- 1 | test_that("gargoyle init(), trigger() and watch() work", { 2 | shiny::reactiveConsole(TRUE) 3 | 4 | s <- shiny::MockShinySession$new() 5 | 6 | init("pif", session = s) 7 | 8 | expect_true( 9 | s$userData$pif() == 0 10 | ) 11 | 12 | # I. Run test for the talkative feature OFF; default option is FALSE 13 | trigger("pif", session = s) 14 | expect_true( 15 | s$userData$pif() == 1 16 | ) 17 | expect_equal( 18 | watch( 19 | "pif", 20 | s 21 | ), 22 | 1 23 | ) 24 | 25 | # II. Run test for the talkative feature ON; set option to TRUE 26 | options("gargoyle.talkative" = TRUE) 27 | expect_output( 28 | trigger("pif", session = s), 29 | regexp = "\\[Gargoyle\\] Triggering pif " 30 | ) 31 | expect_true( 32 | s$userData$pif() == 2 33 | ) 34 | expect_equal( 35 | watch( 36 | "pif", 37 | s 38 | ), 39 | 2 40 | ) 41 | options("gargoyle.talkative" = FALSE) 42 | shiny::reactiveConsole(FALSE) 43 | }) 44 | test_that("gargoyle on() works", { 45 | shiny::reactiveConsole(TRUE) 46 | 47 | s <- shiny::MockShinySession$new() 48 | 49 | init("pif", session = s) 50 | 51 | # I. Run tests for on() without error 52 | expect_identical( 53 | class( 54 | on( 55 | "pif", 56 | { 57 | cat(1 + 1) 58 | }, 59 | session = s 60 | ) 61 | ), 62 | c("Observer.event", "Observer", "R6") 63 | ) 64 | # II. Run tests for on() with errors - uninitialized events 65 | # II.A Provoke error due to uninitialized event - missmatch from init 66 | expect_error( 67 | on( 68 | "pif2", 69 | { 70 | cat(1 + 1) 71 | }, 72 | session = s 73 | ) 74 | ) 75 | # II.B Check error message of uninitialized event - missmatch from init 76 | out_error <- try(on( 77 | "pif2", 78 | { 79 | cat(1 + 1) 80 | }, 81 | session = s 82 | )) 83 | expect_equal( 84 | out_error[[1]], 85 | "Error : [Gargoyle] Flag pif2 hasn't been initiated: can't listen to it.\n" 86 | ) 87 | 88 | # II.C Provoke error due to uninitialized event - missmatch from session arg 89 | expect_error( 90 | on( 91 | "pif", 92 | { 93 | cat(1 + 1) 94 | }, 95 | session = shiny::getDefaultReactiveDomain() 96 | ) 97 | ) 98 | # II.D Check error message of uninitialized event - missmatch session arg 99 | out_error <- try(on( 100 | "pif", 101 | { 102 | cat(1 + 1) 103 | }, 104 | session = shiny::getDefaultReactiveDomain() 105 | )) 106 | expect_equal( 107 | out_error[[1]], 108 | "Error : [Gargoyle] Flag pif hasn't been initiated: can't listen to it.\n" 109 | ) 110 | 111 | shiny::reactiveConsole(FALSE) 112 | }) 113 | -------------------------------------------------------------------------------- /tests/testthat/test-logs.R: -------------------------------------------------------------------------------- 1 | test_that("get_gargoyle_logs() works", { 2 | clear_gargoyle_logs() 3 | expect_equal( 4 | nrow(get_gargoyle_logs()), 5 | 0 6 | ) 7 | expect_equal( 8 | names(get_gargoyle_logs()), 9 | c("what", "time") 10 | ) 11 | .logs$log <- rbind( 12 | .logs$log, 13 | data.frame( 14 | what = c("this"), 15 | time = as.character(Sys.time()), 16 | stringsAsFactors = FALSE 17 | ) 18 | ) 19 | expect_equal( 20 | nrow(get_gargoyle_logs()), 21 | 1 22 | ) 23 | expect_equal( 24 | names(get_gargoyle_logs()), 25 | c("what", "time") 26 | ) 27 | }) 28 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/gargoyle.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "gargoyle" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{gargoyle} 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 | ) 15 | ``` 16 | 17 | ```{r setup} 18 | library(gargoyle) 19 | ``` 20 | 21 | ## About 22 | 23 | `{gargoyle}` is a package that provides wrappers around `{shiny}` to turn your app into and event-based application instead of a full reactive app. 24 | The framework is centered around a `listen` & `trigger` mechanism. 25 | 26 | It works with classical UI, and just needs tweaking the server side of your app. 27 | 28 | ### What the heck? 29 | 30 | `{shiny}`'s default reactive behavior is very helpful when it comes to building small applications. 31 | Because, you know, the good thing about reactivity is that when something moves somewhere, it's updated everywhere. 32 | But the bad thing about reactivity is that when something moves somewhere, it's updated everywhere. 33 | So it does work pretty well on small apps, but can get very complicated on bigger apps, and can quickly get out of hands. 34 | 35 | That's where `{gargoyle}` comes into play: it provides an event based paradigm for building your apps, so that things happen under a control flow. 36 | 37 | ### For whom? 38 | 39 | If you're just building small `{shiny}` apps, you're probably good with `{shiny}` default reactive behavior. 40 | But if ever you've struggled with reactivity on more bigger apps, you might find `{gargoyle}` useful. 41 | 42 | ### The trade-off 43 | 44 | `{gargoyle}` will be more verbose and will demand more work upfront to make things happen. 45 | I believe this is for the best if you're working on a big project. 46 | 47 | ## Design pattern 48 | 49 | `{gargoyle}` has: 50 | 51 | + `init`, `listen` & `trigger`, which allow to initiate, listen on, and trigger an event 52 | 53 | + `on`, that runs the `expr` when the event in triggered 54 | 55 | `gargoyle::trigger()` can print messages to the console using `options("gargoyle.talkative" = TRUE)`. 56 | 57 | ## Example 58 | 59 | ```{r eval = FALSE} 60 | library(shiny) 61 | library(gargoyle) 62 | options("gargoyle.talkative" = TRUE) 63 | ui <- function(request){ 64 | tagList( 65 | h4('Go'), 66 | actionButton("y", "y"), 67 | h4('Output of z$v'), 68 | tableOutput("evt") 69 | ) 70 | } 71 | 72 | server <- function(input, output, session){ 73 | 74 | # Initiating the flags 75 | init("airquality", "iris", "renderiris") 76 | 77 | # Creating a new env to store values, instead of 78 | # a reactive structure 79 | z <- new.env() 80 | 81 | observeEvent( input$y , { 82 | z$v <- mtcars 83 | # Triggering the flag 84 | trigger("airquality") 85 | }) 86 | 87 | on("airquality", { 88 | # Triggering the flag 89 | z$v <- airquality 90 | trigger("iris") 91 | }) 92 | 93 | on("iris", { 94 | # Triggering the flag 95 | z$v <- iris 96 | trigger("renderiris") 97 | }) 98 | 99 | output$evt <- renderTable({ 100 | # This part will only render when the renderiris 101 | # flag is triggered 102 | watch("renderiris") 103 | head(z$v) 104 | }) 105 | 106 | } 107 | 108 | shinyApp(ui, server) 109 | ``` 110 | 111 | You can then get & clear the logs of the times the triggers were called: 112 | 113 | ```{r eval = FALSE} 114 | get_gargoyle_logs() 115 | clear_gargoyle_logs() 116 | ``` 117 | 118 | 119 |
120 | 121 | ## Code of Conduct 122 | 123 | Please note that the gargoyle project is released with a [Contributor Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/). By contributing to this project, you agree to abide by its terms. 124 | --------------------------------------------------------------------------------