{"id":13858080,"url":"https://github.com/MangoTheCat/dynshiny","last_synced_at":"2025-07-13T23:31:08.578Z","repository":{"id":147489544,"uuid":"60483998","full_name":"MangoTheCat/dynshiny","owner":"MangoTheCat","description":"Example Shiny app for dynamically building a UI from a database","archived":false,"fork":false,"pushed_at":"2016-12-15T11:40:16.000Z","size":661,"stargazers_count":59,"open_issues_count":0,"forks_count":14,"subscribers_count":9,"default_branch":"master","last_synced_at":"2024-11-22T16:39:12.032Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"R","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MangoTheCat.png","metadata":{"files":{"readme":"README.Rmd","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2016-06-05T22:23:41.000Z","updated_at":"2024-07-13T02:11:11.000Z","dependencies_parsed_at":"2024-02-09T02:01:19.389Z","dependency_job_id":"c6071aba-e3d8-41d2-b48d-eafa9b07575e","html_url":"https://github.com/MangoTheCat/dynshiny","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/MangoTheCat/dynshiny","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MangoTheCat%2Fdynshiny","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MangoTheCat%2Fdynshiny/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MangoTheCat%2Fdynshiny/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MangoTheCat%2Fdynshiny/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MangoTheCat","download_url":"https://codeload.github.com/MangoTheCat/dynshiny/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MangoTheCat%2Fdynshiny/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265220254,"owners_count":23729784,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-05T03:01:56.042Z","updated_at":"2025-07-13T23:31:08.038Z","avatar_url":"https://github.com/MangoTheCat.png","language":"R","readme":"---\ntitle: Dynamically generated Shiny UI\nauthor: \"Gábor Csárdi — Mango Solutions  \\n Joe Cheng — RStudio\"\nruntime: shiny\n---\n\n## Introduction\n\nIt is not uncommon that the user interface of a Shiny application needs to be\ngenerated dynamically, based on data or program state. One typical use case\nthat we encounter frequently is when the UI lets the user edit a variable number\nof records from a database.\n\nImagine that you have an employee database, where each employee can be\nassigned multiple roles. Each role also has additional data, for\nexample, the proportion of work time the employee is expected to perform\nthat role or a comment field. In a relational database, you would\nstore this information in a `roles` table, where each row corresponds to\none of the role assignments of an employee. When writing a Shiny app to edit\nthe database, it makes sense to edit all roles of an employee on the same\npage: add or delete roles, or modify existing ones. This requires\ngenerating the user interface (UI) of the app dynamically, based on the\ndatabase.\n\n\u003cbr\u003e\n\u003cimg width=\"400\" src=\"./screen1.png\" alt=\"\"\u003e\n\u003cimg width=\"400\" src=\"./screen2.png\" alt=\"\"\u003e\n\u003cbr\u003e\n\n## Requirements\n\nWe want our app to satisfy the following requirements:\n* It must handle multiple employees, i.e. when a new employee is selected\n  from the employee list, it should read in and show all current roles of that\n  employee.\n* It must be able to edit existing roles of an employee, and then update\n  the database.\n* It must be able to add new roles to an employee, and write the updated\n  data to the database.\n* It must be able to delete roles from an employee, and write the updated\n  data to the database.\n* It must be able to handle an arbitrary number of roles for an employee,\n  including no roles at all.\n* It must only modify the database once the user clicks on the `Save`\n  button.\n* It must have a `Cancel` button that discards all edits, and shows the\n  employee roles as last read from the database.\n* The `Save` and `Cancel` buttons must be hidden if the employee data have\n  not been changed.\n\nWhile these requirements are quite straightforward, they are not trivial\nto implement in Shiny. In the rest of this post we build an app that\nimplements them.\n\n## The app\n\n```{r}\nlibrary(shiny)\n```\n\n### The UI part of the app\n\nThe UI definition of the app is quite straightforward, as most of the\ncontent will be dynamically generated. We will have the employee selection\nbox on a side panel, and the roles of the selected employee on the main\npanel.\n\n```{r}\nui \u003c- shinyUI(pageWithSidebar(\n  headerPanel(\"Employee role database\"),\n  sidebarPanel(\n    selectInput(\n      \"employee\",\n      \"Employee\",\n      choices = c(\"Jo Gee\", \"John Doe\")\n    ),\n    uiOutput(\"buttons\")\n  ),\n  mainPanel(\n    uiOutput(\"roles\")\n  )\n))\n```\n\nFor this simple example, we just list all employees here. In practice the\nemployee names come from the database, of course.\n\n`buttons` will contain the `Add`, `Save` and `Cancel` buttons. The last\ntwo are dynamic, as they are only shown if the roles have changed. For \nsimplicity we generate all three buttons dynamically.\n\n### Structure of the app\n\nThis app is different to others, as a significant part of it is\nevent-driven. Many (most?) Shiny apps are purely reactive, i.e. they only\ncontain recipes for how the different output values can be updated, and \nthen it is up to Shiny to make sure that they are updated whenever they\nneed to.\n\nWe found it hard to write this app the traditional way, mainly because the\nUI contains multiple action buttons that trigger dynamic UI changes, and also\nbecause the internal representation of the data must be changed without any\noutput changes.\n\nThe app will have the following main components:\n* We need to store the data that is on the user's screen, and update it,\n  as it changes. This will be in the `data` reactive value.\n* We need to store the data in a database, to be able to compare\n  it to the data under editing and show/hide the 'Save' and 'Cancel' buttons. \n  We'll use the `dbdata()` reactive for this.\n* We will use a `renderUI()` call to create the 'Add', 'Cancel' and 'Save'\n  buttons, as needed.\n* We'll attach events to the 'Add', 'Cancel' and 'Save' buttons, using\n  `observeEvent()`.\n* We'll use a `renderUI()` call to create the UI for the records, with a \n  helper function, `createRecord()`, that creates a single record.\n\n### Triggering UI changes\n\nTo trigger UI changes as needed, we introduce a reactive trigger construct:\n```{r}\nmakeReactiveTrigger \u003c- function() {\n   rv \u003c- reactiveValues(a = 0)\n   list(\n     depend = function() {\n       rv$a\n       invisible()\n     },\n     trigger = function() {\n       rv$a \u003c- isolate(rv$a + 1)\n     }\n   )\n }\n```\n`makeReactiveTrigger` creates reactive triggers. A reactive trigger has\ntwo parts: \n1. `$depend()` can be used within reactive expressions to declare that\n   the reactive expression must be updated whenever the trigger sets off.\n2. `$trigger()` sets off the trigger.   \nFor the purpose of this post it is not very important to know how a reactive\ntrigger works. It is sufficient to know that whenever it is `trigger()`-ed,\nall the `depend()`ent reactives are updated.\n\n### The `server` function\n\nWe are ready to write the more complicated `server` function.\n\nWe will use the `data` reactive value to store the current values of the\nroles. `data` is updated whenever the input widgets change (see later, \nwhen we create these widgets in `createRecord()`). \n\nWe assume that `data` is a data frame and each role corresponds to a row\nin it. For this simple app `data` has columns `id` and `role` only. Other\nmetadata can be easily added as additional columns. The `id` field is a\nsimple numeric id of the employee. For now we set `data` to `NULL`. It \nwill be automatically updated to the first (=default) employee's data \nwhen the app loads.\n\n```{r server-1, eval = FALSE, purl = TRUE}\nserver \u003c- function(input, output, session) {\n  rvs \u003c- reactiveValues(data = NULL)\n  db_dir \u003c- \".\"\n```\n\n```{r echo = FALSE, results = \"hide\"}\n  ## We work in a temporary directory for the public deployment\n  db_dir \u003c- tempfile()\n  dir.create(db_dir)\n  file.copy(list.files(pattern = \"*.csv\"), db_dir)\n```\n\nWe'll use `uiTrigger` to trigger a UI rebuild for the records, this trigger is the heart of the app.\n`fileTrigger` is used to trigger an update of `dbdata()`, i.e. to (re)read the data from the database.\n\n```{r server-2, eval = FALSE, purl = TRUE}\n  uiTrigger \u003c- makeReactiveTrigger()\n  fileTrigger \u003c- makeReactiveTrigger()\n```\n\n`dbdata()` provides the last version of the employee records as they are in the database.\nIt is updated whenever a new employee is selected (`input$employee`) and an update\ncan also be triggered via `fileTrigger`.\n\nFor simplicity, we assume that each employee's data is stored in a CSV file\nthat is named according to the employee. It is easy to change this to a\nproper database query.\n\n```{r server-3, eval = FALSE, purl = TRUE}\n  dbdata \u003c- reactive({\n    cat(\"i reading input file\\n\")\n    fileTrigger$depend()\n    req(input$employee)\n    filename \u003c- file.path(db_dir, paste0(input$employee, \".csv\"))\n    read.csv(filename, stringsAsFactors = FALSE)\n  })\n```\n\nIf new data is read from the database, then we also need to trigger a UI rebuild.\nFor this we simply put an event handler on the `dbdata()` reactive. This runs \nevery time the reactive is updated.\n\n```{r server-4, eval = FALSE, purl = TRUE}\n  observeEvent(dbdata(), {\n    rvs$data \u003c- dbdata()\n    uiTrigger$trigger()\n  })\n```\n\n### Dynamic `Cancel` and `Save` buttons\n\nThe `Add` button is always shown. The `Cancel` and `Save` buttons are only\nshown if `data` and `dbdata` are not the same.\n\n```{r server-5, eval = FALSE, purl = TRUE}\n  dataSame \u003c- reactive({\n    identical(rvs$data, dbdata())\n  })\n\n  output$buttons \u003c- renderUI({\n    div(\n      actionButton(inputId = \"add\", label = \"Add\"),\n      if (! dataSame()) {\n        span(\n          actionButton(inputId = \"cancel\", label = \"Cancel\"),\n          actionButton(inputId = \"save\", label = \"Save\",\n                       class = \"btn-primary\")\n        )\n      } else {\n        span()\n      }\n    )\n  })\n```\n\n### Add reactivity\n\nSo again, parts of this app are event-driven. We specify what should\nhappen whenever the user presses the various action buttons or edits\nthe roles.\n\nThe first event we need to handle is adding a new role. We create a new id \nfor it first and then just add it to the bottom of the data frame that holds\nthe data. Then we trigger a UI rebuild.\n\n```{r server-6, eval = FALSE, purl = TRUE}\n  observeEvent(input$add, {\n    cat(\"i adding a new record\\n\")\n    newid \u003c- if (nrow(rvs$data) == 0) {\n      1L\n    } else {\n      max(as.integer(rvs$data$id)) + 1L\n    }\n    rvs$data \u003c- rbind(rvs$data, list(id = newid, role = \"\"))\n    uiTrigger$trigger()\n  })\n```\n\nWhen the `Cancel` button is hit, we need to restore the data from\n`dbdata()`. Then we trigger a UI rebuild. This is not always needed\nbut it is the simplest way to make sure that the UI shows the current\ndata.\n\n```{r server-7, eval = FALSE, purl = TRUE}\n  observeEvent(input$cancel, {\n    cat(\"i cancelling edits\\n\")\n    rvs$data \u003c- dbdata()\n    uiTrigger$trigger()\n  })\n```\n\nThe `Save` button is also simple. We write out the file and make sure that \nthe `dbdata()` reactive is updated using `fileTrigger`. \nThis will also trigger an unnecessary UI rebuild in the end but we\ncan live with that.\n\n```{r server-8, eval = FALSE, purl = TRUE}\n  observeEvent(input$save, {\n    cat(\"i saving to file\\n\")\n    filename \u003c- file.path(db_dir, paste0(input$employee, \".csv\"))\n    write.csv(rvs$data, filename, quote = FALSE, row.names = FALSE)\n    fileTrigger$trigger()\n  })\n```\n\n### The main dynamic UI\n\nThe next part is the main UI that contains the employee roles.\nWe use `uiTrigger$depend()` to denote that this `renderUI` expression\nneeds to run whenever a UI rebuild is triggered.\n\nNote the use of `isolate`. We do not want `output$roles` to depend on\n`rvs$data` directly, because we only want to rebuild the UI after selected\nevents, but not all data changes. E.g. if the user just edits a text input,\nno UI rebuild is needed.\n\nWe use `create_role` to create the UI and (possibly) the event wiring for\neach role. Its first argument is the widget id, a number between `1` and\n`n`, where `n` is the number of roles on the screen.\n\n```{r server-9, eval = FALSE, purl = TRUE}\n  output$roles \u003c- renderUI({\n    cat(\"i rebuild the UI\\n\")\n    uiTrigger$depend()\n    mydata \u003c- isolate(rvs$data)\n    w \u003c- lapply(seq_len(nrow(mydata)), function(i) {\n      create_role(i, mydata[i, ])\n    })\n    do.call(fluidRow, w)\n  })\n```\n\n### Creating the UI for a role\n\nThis is another key part of the app and it is also the part that is easy to\nwrite incorrectly. `create_role` is a closure, a function that creates both\na function and an environment to store data.\n\nWe need the environment to store the maximum number of widgets that were\nwired up with edit and delete events. We need this because in Shiny it is\nnot (easily) possible to remove bindings (i.e. `observeEvent` triggers). So\neven if we rebuild the UI and remove some elements, the previously created\ntriggers will still be in effect, and recreating them will trigger duplicate\nevents.\n\n```{r server-10, eval = FALSE, purl = TRUE}\n  create_role \u003c- (function() {\n\n    inited \u003c- 0\n\n    function(wid, record) {\n      w \u003c- div(wellPanel(\n        textInput(\n          paste0(\"inp-\", wid),\n          label = record$id,\n          value = record$role\n        ),\n        actionButton(\n          paste0(\"del-\", wid),\n          label = \"Delete\",\n          class = \"btn-danger\"\n        )\n      ))\n```\n\nSo every time we build a widget with a given id (`wid`) number we only create new `observeEvent`\ntriggers if the widget's events weren't already wired up.\n\nIn other words, the newly built UI will reuse as many of the existing wired\nwidgets as possible. `inited` stores the number of `wired` widgets and the\nid's of their inputs and delete buttons are `inp-x` and `del-x`, where `x`\nis a number between 1 and `inited`.\n\nNote that editing the text input field does not trigger a UI rebuild and\nthis is intentional. We don't want rebuilds just because the user has typed\nin something new in the input field.\n\n```{r server-11, eval = FALSE, purl = TRUE}\n      if (wid \u003e inited) {\n        observeEvent(input[[paste0(\"inp-\", wid)]], {\n          rvs$data[wid, \"role\"] \u003c- input[[paste0(\"inp-\", wid)]]\n        })\n\n        observeEvent(input[[paste0(\"del-\", wid)]], {\n          rvs$data \u003c- rvs$data[-wid, , drop = FALSE]\n          uiTrigger$trigger()\n        })\n```\n\nWe need to update `inited` if we created wiring for a new widget.\n\n```{r server-12, eval = FALSE, purl = TRUE}\n\t    inited \u003c\u003c- wid\n      }\n\n      w\n    }\n  })()\n}\n```\n\nNote that here we create a function and call it immediately. \nThe function itself creates and returns a function, which we\nassign to `create_role`. The effect of this is that every time\nwe run `create_role`, it has access to the same `inited` variable\nand updates it as needed. `inited` is in the parent environment of\n`create_role`.\n\nAnother non-obvious observation is that (by default) `observeEvent`\nevaluates the expression in the environment of its caller, which is the\nexecution environment of the running`create_role` function.\nExecution environments are usually temporary but in this case\nthe `observeEvent` expression keeps a reference to it, so it is kept\nalive. This environment stores the value of `wid` at the time\nthe event handler was created. This way, every event handler expression\nrefers to its own `wid` value.\n\nWe are now ready to start the app.\n\n```{r server-13, eval = FALSE, purl = TRUE}\nshinyApp(ui, server, options = list(height = 1600))\n```\n\n## Summary\n\nIt took me (Gábor) a couple of attempts to write the first version of this\nsmall Shiny app. My initial, purely reactive (i.e. without reactive \nvalues and triggers) attempts all failed. Then Joe helped me simplify \nit, introduced the reactive trigger expression and gave me important\ninsight about imperative and reactive apps.\n\nWhile we expressed much of the app imperatively, it is important to understand\nthat there isn't an either/or relationship between imperative and reactive.\nWhat we want to do is identify which pieces of state need to be treated\nimperatively, and which can still be handled reactively.\n\nThe question you have to answer is: \"Can this state be derived/computed \nfrom another state already represented in the system?\" If so, it's a strong \ncandidate for being made into a reactive expression instead.\n\n## Exercises\n\n1. Since we wrote the first version of the app, Shiny introduced\n`insertUI` and `removeUI`. These are probably better alternatives\nto our `create_role` closure. You need to know a tiny bit about\nCSS selectors to use them. Modify the app to use `insertUI` and \n`removeUI`.\n\n2. (Hard.) Write a reusable Shiny module that encapsulates the complexity\nof this problem. Maybe the module could have the following parameters:\n    * function to read in the data,\n    * function to save the data,\n    * function to build the UI of a single record, from the data, and an\n      object id.\n\n## Feedback\n\nWe would be very excited to hear about improvements or alternative\nsolutions to this problem. Should you have one, please open an issue\nin the https://github.com/MangoTheCat/dynshiny repository. Thank you!\n\n### Try the app\n\nTo try the app, go to https://mangothecat.shinyapps.io/dynshiny/.\n","funding_links":[],"categories":["R"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMangoTheCat%2Fdynshiny","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FMangoTheCat%2Fdynshiny","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMangoTheCat%2Fdynshiny/lists"}