Rhino R Package Tutorial: Build Your First Rhino App
The standard procedure to create a Shiny app is straightforward. It involves a single app.R with a ui.R and server.R. But this simplicity also makes it difficult to build <strong>production-grade Shiny apps</strong>. At Appsilon, we have something different in our toolbox; we use <strong>Rhino</strong>. TOC: <ul><li><a href="#what">What is Rhino?🦏</a></li><li><a href="#similar">Similar options to the Rhino R package🟰</a></li><li><a href="#installing">How to install Rhino⬇️</a></li><li><a href="#use">How to use Rhino🧰</a></li><li><a href="#example">Example Shiny build using Rhino🤷</a></li><li><a href="#modules">Let's talk Modules and Rhino⚙️</a></li><li><a href="#libraries">Managing Libraries with Rhino🔖</a></li><li><a href="#chart">Chart example 📈</a></li><li><a href="#table">Table example 🔨</a></li><li><a href="#logic">Finishing touches: Logic🤖</a></li><li><a href="#style">Finishing touches: Style🎨</a></li><li><a href="#complete">Complete Shiny app with Rhino🪄</a></li></ul> <hr /> <h2 id="what">What is Rhino anyway?🦏</h2> Rhino is an opinionated framework focusing on best practices and development tools for R Shiny developers. The origins of the Rhino R package start from an internal need at Appsilon to avoid repetitive tasks, unify architecture, and codify our practices. Rhino’s core benefits to our R/Shiny development process: <ol><li style="font-weight: 400;" aria-level="1">Save time and avoid repetitive tasks by including best practices we value at the start of a project</li><li style="font-weight: 400;" aria-level="1">Unify applications’ architecture by providing sensible defaults.</li><li style="font-weight: 400;" aria-level="1">Automate and codify Appsilon practices to pass on knowledge in the form of code</li></ol> Over the years, we took our collective experience across projects - noting challenges and what worked and didn’t work for us as a team. We built internal tools to address these issues and help structure our projects for faster, more successful outcomes. Now that the Rhino project has evolved into an R package we are excited to share it with the Shiny community. Please note that Rhino is in its early stages. We hope that by making the package public we can achieve two things. Firstly, share our knowledge base with the community and secondly, receive feedback from users. We invite you to test out Rhino and submit feedback. <h2 id="similar">Similar options to the Rhino R package🟰</h2> Rhino was built with our enterprise dev needs in mind. But we believe ‘biodiversity’ is key for a healthy Shiny ecosystem. And that means one solution isn’t the right fit for every project. There are other options from workflow packages to well-established toolkits. If you’re searching for the right solution, we encourage you to check out the options listed below. The following mentions common solutions and how Rhino differs: <ul><li style="font-weight: 400;" aria-level="1"><b>golem</b>: Rhino apps are not R packages. Rhino puts more emphasis on development tools, clean configuration, and minimal boilerplate and tries to provide default solutions for typical problems and questions in these areas.</li><li style="font-weight: 400;" aria-level="1"><b>leprechaun</b>: Leprechaun works by scaffolding Shiny apps, without adding dependencies. Rhino minimizes generated code and aims to provide a complete foundation for building Shiny apps ready for deployment in enterprise so that you can focus on the application’s logic and user experience.</li><li style="font-weight: 400;" aria-level="1"><b>devtools</b>: devtools streamlines package development. Rhino is a complete framework for building Shiny apps. Rhino features are interdependent (e.g. coverage and unit tests) and cannot be used without making the app into a basic Rhino structure.</li><li style="font-weight: 400;" aria-level="1"><b>usethis</b>: usethis adds independent code snippets you ask it to. Rhino is a complete framework for building Shiny apps. Your app is designed to call Rhino functions instead of having them insert code into your project.</li></ul> Each of these has value, and depending on the project may be a more appropriate option. If you need assistance feel free to reach out to us to get your team on the right path. <h2 id="installing">How to install the Rhino package?⬇️</h2> To install the Rhino package run: <pre><code> install.packages(“rhino”) </code></pre> <h2 id="use">How to use Rhino?🧰</h2> Whether you are starting a new project or migrating an existing app - using Rhino is straightforward. <h3>The simple-r method🤯</h3> If you use RStudio, probably the easiest way to create a new Rhino application is to simply use the <b>Create New Project</b> feature. Once Rhino is installed, it will be automatically added as one of the options in RStudio. Choose it, input the new project name, and you are ready to go. <h3>The simple method🙂</h3> To initialize a new Rhino project, run the init function: <pre><code> rhino::init(“RhinoApplication”) </code></pre> In running the app this way, Rhino will not change your working directory (wd). To do so, you will need to open a new R session in your new application directory or manually change the wd. <h2 id="example">Example Shiny build using Rhino🤷</h2> Now, we will build a simple app about… you guessed it: <b>Rhinos! 🦏</b> If everything is set up correctly, you will have the following files in your directory: <img class="alignnone wp-image-17712" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01be51887a9fd4c53593b_rhino-shiny-app-directory.webp" alt="Rhino Shiny file directory " width="800" height="645" /> <h3>Running a Shiny application▶️</h3> To run your newly minted Rhino application, you have to use the following command: <pre><code> shiny::runApp() </code></pre> It does not get simpler than this, does it? In any case, if you followed all the steps, you should be able to run the application successfully, and it should have the standard “Hello” message on the screen. <h3 id="modules">Let's talk Modules and Rhino⚙️</h3> Modules are R/Shiny’s way of keeping things simple, and Rhino capitalizes on that ability. In short, modules help you keep a logical division between different parts of the apps. For example, in an application that serves a map as well as a barplot, in most cases, it would make sense to have separate modules for both of these. Also, since R/Shiny relies heavily on namespacing correctly, modules resolve this naturally and solve it without you worrying about it. We don’t have to dive deep into modules here, but if you are curious, here is more about it on the official <a href="https://shiny.rstudio.com/articles/modules.html" target="_blank" rel="nofollow noopener">RStudio Shiny Documentation</a>. In Rhino, each application view is intended to live as a <a href="https://shiny.rstudio.com/articles/modules.html" target="_blank" rel="nofollow noopener">Shiny module</a> and use encapsulation provided by the {<a href="https://klmr.me/box/" target="_blank" rel="nofollow noopener">box} package</a>. <h4>{box}-ed in📦</h4> The {box} library makes it incredibly easy to divide your code into logical modules. In other words, it enables modularization by giving you the ability to treat each kind of functionality in an isolated way. Imagine creating local libraries for your code that have functions your app needs. Now, imagine if the function is only used in two places instead of your entire app. In vanilla R/Shiny you would have to rely on loading the functions globally using something like a global.Rfile. {box} makes it possible for you to load the functions and variables only where you need them. That is a lot of words to suggest something as follows. Let’s say we have a function that drills down into the sales data. Let’s assume it’s called drill_down_sales()and this function is exported using @export from a file called sales_utils.R. Now if we have two modules: plot and header, out of them, only plot seems to need this function. We can then use box::use(sales_utils[drill_down_sales]) in the mod_plot.R file. The function will only be available to this file in question and it would make our imports simpler. If you are familiar with Python, think about how we often use the from LIBRARY import FUNCTION. That is what {box} allows us to do. <h3>Building our first Module🏗️</h3> To begin, we will build our first module in the app/view/ directory. We can do that by using the following code block and for now, we don’t have to worry about actually building the chart: <pre><code> # app/view/chart.R <br>box::use( shiny[h3, moduleServer, NS], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> h3("Chart") } <br>#' @export server <- function(id) { moduleServer(id, function(input, output, session) { print("Chart module server part works!") }) } </code></pre> Look how it all comes together: the {box} usage, the R/Shiny moduleServer command, the NS() function to resolve the namespace. <h3>Calling a Module📶</h3> To call a module, we first need to import it into the main.R. How do we do that? Let’s use {box} once again: <pre><code> # app/main.R <br>box::use( app/view/chart, ) <br>... </code></pre> Once imported, the chart module will be ready for us in the main.R. We can then call each of its ui and server components using chart$ui() and chart$server(). They should work naturally since that is how everything is structured in Rhino and how it leverages {box}. Let’s import things to the main.R, and call the functions from our chart module: <pre><code> # app/main.R <br>box::use( shiny[bootstrapPage, moduleServer, NS], ) box::use( app/view/chart, ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> bootstrapPage( chart$ui(ns("chart")) ) } <br>#' @export server <- function(id) { moduleServer(id, function(input, output, session) { chart$server("chart") }) } </code></pre> <h3 id="libraries">Managing Libraries with Rhino🔖</h3> Alright. So now we know how we can create modules and import them within the files of our Rhino project. But the power of programming is not in making everything yourself or reinventing the wheel, but rather using what is already made. What would an R project without {tidyverse} even look like!? In Rhino, we rely on the {renv} package to manage these dependencies, and we have a separate dependencies.R where we can simply define what we rely on. To install a library, all you have to do is something like the following in the R Console: <pre><code> # In R console renv::install(c("dplyr", "echarts4r", "htmlwidgets", "reactable", "tidyr")) </code></pre> Then, we simply need to import all these packages using the library() call in dependencies.R: <pre><code> # dependencies.R <br># This file allows packrat (used by rsconnect during deployment) to pick up dependencies. library(dplyr) library(echarts4r) library(htmlwidgets) library(reactable) library(rhino) library(tidyr) </code></pre> But how do we make sure our packages are available when someone else uses the same project or when we deploy it on a server? We take a snapshot! <pre><code> # in R console renv::snapshot() </code></pre> The renv::snapshot() command will simply pick up each of the packages imported in dependencies.R and create a renv.lock file. This file will have every package, along with the repository such as CRAN, MRAN, or others along with the version number and details for the library/package as well. Of course, you can also include local packages this way! To add the dependencies to a module, you simply use the {box} package again. <pre><code> # app/view/chart.R <br>box::use( echarts4r, shiny[h3, moduleServer, NS, tagList], ) <br>... </code></pre> <h3 id="chart">Let's build a Chart!📈</h3> Now that the chart.R is all ready, we can use the {echarts4r} import and develop the plot that we need to display. <pre><code> # app/view/chart.R <br>box::use( echarts4r, shiny[h3, moduleServer, NS, tagList], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> tagList( h3("Chart"), echarts4r$echarts4rOutput(ns("chart")) ) } <br>#' @export server <- function(id) { moduleServer(id, function(input, output, session) { output$chart <- echarts4r$renderEcharts4r( # Datasets are the only case when you need to use :: in `box`. # This issue should be solved in the next `box` release. rhino::rhinos |> echarts4r$group_by(Species) |> echarts4r$e_chart(x = Year) |> echarts4r$e_line(Population) |> echarts4r$e_x_axis(Year) |> echarts4r$e_tooltip() ) }) } </code></pre> The code above will simply use the dataset and plot the Rhino data as a line chart which would look something like the following: <img class="alignnone size-full wp-image-17710" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01be685f60a598abd2426_rhino-line-chart.webp" alt="Rhino line chart" width="1600" height="463" /> <h3 id="table">Let's build a Table!🔨</h3> By now, we believe you have caught the gist of it. To create a table, we would go back to creating a new module. So, let us create app/view/table.R. Here is what you can use to build it. <pre><code> # app/view/table.R <br>box::use( shiny[h3, moduleServer, NS, tagList], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> tagList( h3("Table") ) } <br>#' @export server <- function(id) { moduleServer(id, function(input, output, session) { <br> }) } </code></pre> …and when you call it to the main.R, it would follow suit as well! <pre><code> # app/main.R <br>box::use( shiny[bootstrapPage, moduleServer, NS], ) box::use( app/view/chart, app/view/table, ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> bootstrapPage( table$ui(ns("table")), chart$ui(ns("chart")) ) } <br>#' @export server <- function(id) { moduleServer(id, function(input, output, session) { table$server("table") chart$server("chart") }) } </code></pre> It’s getting simpler, isn’t it? <b>To Summarise: </b>A new feature equals a new module that goes into app/view/. Each module then is imported into the main.R using {box} and once done, it can be used in the UI and server as module$ui() and module$server(). The amount of time this saves once set up correctly is wonderful. In fact, in more advanced usage, you can even call modules within modules and then call the parent module into the main. The possibilities are practically endless, and we expect you to go the extra mile in finding them! <h4>Build a Table (for real this time)👀</h4> In any case, for now, let’s update the table.R to actually build a table. For that, first, we will update the main.R. <pre><code> # app/main.R <br>box::use( shiny[bootstrapPage, moduleServer, NS], ) box::use( app/view/chart, app/view/table, ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> bootstrapPage( table$ui(ns("table")), chart$ui(ns("chart")) ) } <br>#' @export server <- function(id) { moduleServer(id, function(input, output, session) { # Datasets are the only case when you need to use :: in `box`. # This issue should be solved in the next `box` release. data <- rhino::rhinos <br> table$server("table", data = data) chart$server("chart", data = data) }) } </code></pre> We are now using the same dataset in the two modules. If you are feeling experimental, you can even have modules return values and then use them in the other modules. Rhino does not break the standard R/Shiny reactivity. In fact, it enhances it. All your modules can share resources, talk to each other and achieve cool things together! Now, we update the table.R: <pre><code> # app/view/table.R <br>box::use( shiny[h3, moduleServer, NS, tagList], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> tagList( h3("Table") ) } <br>#' @export server <- function(id, data) { moduleServer(id, function(input, output, session) { <br> }) } </code></pre> …and also, the chart.R: <pre><code> # app/view/chart.R <br>box::use( echarts4r, shiny[h3, moduleServer, NS, tagList], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> tagList( h3("Chart"), echarts4r$echarts4rOutput(ns("chart")) ) } <br>#' @export server <- function(id, data) { moduleServer(id, function(input, output, session) { output$chart <- echarts4r$renderEcharts4r( data |> echarts4r$group_by(Species) |> echarts4r$e_chart(x = Year) |> echarts4r$e_line(Population) |> echarts4r$e_x_axis(Year) |> echarts4r$e_tooltip() ) }) } </code></pre> Since both modules now use the same data source, we can use the function parameter data to achieve our logic. Also, let’s now use {reactable} to finally build the table!: <pre><code> # app/view/table.R <br>box::use( reactable, shiny[h3, moduleServer, NS, tagList], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> tagList( h3("Table"), reactable$reactableOutput(ns("table")) ) } <br>#' @export server <- function(id, data) { moduleServer(id, function(input, output, session) { output$table <- reactable$renderReactable( reactable$reactable(data) ) }) } </code></pre> The app will start to look like this: <img class="alignnone wp-image-17716" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01be8ccf2ef0d95c21304_rhino-table-built-with-reactable.webp" alt="Rhino table built with reactable" width="1200" height="584" /> The cool thing here is that if you want to modify the plot, you now have a specific file (or module!) to go to, and if you want to move the table in a different way, you know you can go to its module. When you are inclined to change the overall layout or structure, you have the main.R to edit! Isn’t that simple? This is the power of Rhino! <h3 id="logic">Finishing touches: Shiny app Logic 🤖</h3> Now that we are done with the core content of the app, it would make sense to add some interaction to it. That is where the logic side of things comes into play. Let’s try to transform the data a bit. Ideally, we want to show each species in a separate column to compare them properly in the table. Let’s create a file called app/logic/data_transformation.R: <pre><code> # app/logic/data_transformation.R <br>box::use( tidyr[pivot_wider], ) <br>#' @export transform_data <- function(data) { pivot_wider( data = data, names_from = Species, values_from = Population ) } </code></pre> Now, we need to call the function in the table module using the same box::use syntax we have been using so far. <pre><code> # app/view/table.R <br>box::use( reactable, shiny[h3, moduleServer, NS, tagList], ) box::use( app/logic/data_transformation[transform_data], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> tagList( h3("Table"), reactable$reactableOutput(ns("table")) ) } <br>#' @export server <- function(id, data) { moduleServer(id, function(input, output, session) { output$table <- reactable$renderReactable( data |> transform_data() |> reactable$reactable() ) }) } </code></pre> Once it’s done, you should now have a table that looks like the following: <img class="alignnone wp-image-17714" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01be84c2c8ea263de6762_rhino-table-arranged-by-population.webp" alt="Reactable Rhino table arranged by population" width="1200" height="580" /> Something seems off though. The table is arranged by the Black Rhino population. Ideally, it should be arranged by year. Let’s use {dplyr} for that and modify data_transformation.R: <pre><code> # app/logic/data_transformation.R <br>box::use( dplyr[arrange], tidyr[pivot_wider], ) <br>#' @export transform_data <- function(data) { pivot_wider( data = data, names_from = Species, values_from = Population ) |> arrange(Year) } </code></pre> The result? A table that makes more sense in terms of information. <img class="alignnone wp-image-17718" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01beafb4af0620510d073_rhino-table-dplyr-data_transformation.webp" alt="Reactable Rhino table using dplyr for data_transformation" width="1200" height="584" /> But there is still something off. The graph shows comma separators in the x-axis, which is actually a list of years. We do not use separators in these but in the R/Shiny world, nothing is impossible. Let’s create a new file called app/logic/chart_utils.R: <pre><code> # app/logic/chart_utils.R <br>box::use( htmlwidgets[JS], ) <br>#' @export label_formatter <- JS("(value, index) => value") </code></pre> Then, we add it to the chart module: <pre><code> # app/view/chart.R <br>box::use( echarts4r, shiny[h3, moduleServer, NS, tagList], ) box::use( app/logic/chart_utils[label_formatter], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> tagList( h3("Chart"), echarts4r$echarts4rOutput(ns("chart")) ) } <br>#' @export server <- function(id, data) { moduleServer(id, function(input, output, session) { output$chart <- echarts4r$renderEcharts4r( data |> echarts4r$group_by(Species) |> echarts4r$e_chart(x = Year) |> echarts4r$e_line(Population) |> echarts4r$e_x_axis( Year, axisLabel = list( formatter = label_formatter ) ) |> echarts4r$e_tooltip() ) }) } </code></pre> <img class="alignnone wp-image-17706" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01bead6bd062d7ff5d9a7_chart_utils-sans-comma.webp" alt="Reactable Rhino table added to chart module" width="1200" height="574" /> All in all, we have added a logic layer to our modules and both of them have received several improvements. Logic layers can be more extensive and detailed than just changing the format or adding some transformation. In fact, for more complex Shiny apps, Rhino makes it easier for you to integrate multiple pieces of logic into your modules with ease while keeping a logical separation between functions. For example, regardless of which module your functions are used in, all related functions remain in the same app/logic file. This makes it easier to maintain the code and make functions talk to each other. <h3 id="style">Finishing touches: Shiny app Style🎨</h3> Our app works but does it look great? Not yet. Right now it looks a bit barebones and we can change that easily! Rhino allows you to use sass using the {sass} package in R. </span> <h4>Adding some Sass💁</h4> <b>Note: </b>The Rhino SASS builder uses Node.js. To run it without Node, you can change the sass label’s value from “node” to “r” in the rhino.yml file. This will make the builder leverage the R package for the SASS building. However, it uses a deprecated C++ library, so we feel the Node solution is the default and it is also our recommendation. Rhino helpfully offers an app/styles directory to house all your SASS files as well as any partials you create. Where there is CSS (or SASS) there are classes and ids. Let’s add some to our project. First, we will add a class “components-container” to the main.R file: <pre><code> # app/main.R <br>box::use( shiny[bootstrapPage, div, moduleServer, NS], ) box::use( app/view/chart, app/view/table, ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> bootstrapPage( div( class = "components-container", table$ui(ns("table")), chart$ui(ns("chart")) ) ) } <br>#' @export server <- function(id) { moduleServer(id, function(input, output, session) { # Datasets are the only case when you need to use :: in `box`. # This issue should be solved in the next `box` release. data <- rhino::rhinos <br> table$server("table", data = data) chart$server("chart", data = data) }) } </code></pre> Now, we will add “component-box” to the chart.R file in app/view: <pre><code> # app/view/chart.R <br>box::use( echarts4r, shiny[div, moduleServer, NS], ) box::use( app/logic/chart_utils[label_formatter], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> div( class = "component-box", echarts4r$echarts4rOutput(ns("chart")) ) } <br>#' @export server <- function(id, data) { moduleServer(id, function(input, output, session) { output$chart <- echarts4r$renderEcharts4r( data |> echarts4r$group_by(Species) |> echarts4r$e_chart(x = Year) |> echarts4r$e_line(Population) |> echarts4r$e_x_axis( Year, axisLabel = list( formatter = label_formatter ) ) |> echarts4r$e_tooltip() ) }) } </code></pre> And now, the same class as above to the table.R file or the table module: <pre><code> # app/view/table.R <br>box::use( reactable, shiny[div, moduleServer, NS], ) box::use( app/logic/data_transformation[transform_data], ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> div( class = "component-box", reactable$reactableOutput(ns("table")) ) } <br>#' @export server <- function(id, data) { moduleServer(id, function(input, output, session) { output$table <- reactable$renderReactable( data |> transform_data() |> reactable$reactable() ) }) } </code></pre> Let’s write some CSS for the classes now. You can house the SASS or .scss files in app/styles/main.scss to begin with. <pre><code> // app/styles/main.scss <br>.components-container { display: inline-grid; grid-template-columns: 1fr 1fr; width: 100%; <br> .component-box { padding: 10px; margin: 10px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } } </code></pre> But if you try running the app after making the change above, you will notice nothing has changed. If you remember our Note from this section, this is where the building of SASS comes into play. <pre><code> # in R console rhino::build_sass() </code></pre> It should now look something like the following: <img class="alignnone wp-image-17720" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01bec832c47d673103dfe_sass-styled-rhino-app.webp" alt="Sass styled Rhino application" width="1200" height="333" /> Looks much neater, doesn’t it? We have the plots in separate boxes and they seem like they give different pieces of information about the same topic. Let’s now add a title to the application by changing the main.R: <pre><code> # app/main.R <br>box::use( shiny[bootstrapPage, div, h1, moduleServer, NS], ) box::use( app/view/chart, app/view/table, ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> bootstrapPage( h1("RhinoApplication"), div( class = "components-container", table$ui(ns("table")), chart$ui(ns("chart")) ) ) } <br>... </code></pre> And let’s add some styling again: <pre><code> // app/styles/main.scss <br>.components-container { display: inline-grid; grid-template-columns: 1fr 1fr; width: 100%; <br> .component-box { padding: 10px; margin: 10px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } } <br>h1 { text-align: center; font-weight: 900; } </code></pre> Of course, we need to build the SASS again using build_sass(): <pre><code> # in R console rhino::build_sass() </code></pre> <img class="alignnone wp-image-17722" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01bed817460dff134798b_sass-styled-rhino-app-with-title.webp" alt="Sass styled Rhino app with title" width="1200" height="373" /> It is important to note that Rhino takes care of adding app/static/app.min.css to the application header so there is no need for you to do so. <h4>Interaction with JS🚀</h4> <b>Note: </b>Rhino requires Node.js for this as well. You can still use regular JavaScript code but please ensure you add it to the app/static/js file and not the www/ folder like you would for vanilla JS. Let’s add a button: <pre><code> # app/main.R <br>box::use( shiny[bootstrapPage, div, h1, icon, moduleServer, NS, tags], ) box::use( app/view/chart, app/view/table, ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> bootstrapPage( h1("RhinoApplication"), div( class = "components-container", table$ui(ns("table")), chart$ui(ns("chart")) ), tags$button( id = "help-button", icon("question") ) ) } <br>... </code></pre> Let’s style it using its id (help-button): <pre><code> // app/styles/main.scss <br>.components-container { display: inline-grid; grid-template-columns: 1fr 1fr; width: 100%; <br> .component-box { padding: 10px; margin: 10px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } } <br>h1 { text-align: center; font-weight: 900; } <br>#help-button { position: fixed; top: 0; right: 0; margin: 10px; } </code></pre> <b>Pro Tip: </b>You need to build_sass() after every change to the SASS files. But there is another trick. If you create a new terminal, you can start an R instance in it and call rhino::build_sass() in watch mode using rhino::build_sass(watch = TRUE). As long as the terminal is active, it will continue to watch for changes in the SASS files (on every save). <img class="alignnone wp-image-17704" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01bedafc0ff7c6577adca_build-sass.webp" alt="Sass build pro-tip" width="1200" height="380" /> In any case, once you add the styling to the button and build_sass(), it should show up on the app as it does in the screenshot below: <img class="alignnone size-full wp-image-17722" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01bed817460dff134798b_sass-styled-rhino-app-with-title.webp" alt="Sass styled Rhino app with title" width="1600" height="497" /> Let’s write the JS code to show a popup alert: <pre><code> // app/js/index.js <br>export function showHelp() { alert('Learn more about Rhino: https://appsilon.github.io/rhino/'); } </code></pre> If you’re familiar with JS, you may have noticed “export” being used before the function. In Rhino, you can write as many JS functions as you want, but only those with the keyword at the beginning will be available for the app. This extends the flexibility by you being able to experiment, only use certain functions, and try different approaches! Now, just like with styles, you need to build the JS using rhino::build_js() 🥸Psst, the Pro-tip about the watch mode applies here, too. By building both SASS and JS, we are essentially creating the app.min.css and app.min.js files which are minified versions of all the available styles and interaction code respectively. Both of these are automatically added to the <head> tag and you do not need to call them explicitly. Let’s now call the function showHelp() in the main.R file: <pre><code> # app/main.R <br>box::use( shiny[bootstrapPage, div, h1, icon, moduleServer, NS, tags], ) box::use( app/view/chart, app/view/table, ) <br>#' @export ui <- function(id) { ns <- NS(id) <br> bootstrapPage( h1("RhinoApplication"), div( class = "components-container", table$ui(ns("table")), chart$ui(ns("chart")) ), tags$button( id = "help-button", icon("question"), onclick = "App.showHelp()" ) ) } <br>... </code></pre> Where did the “App” come from in App.showHelp()? This is the second important difference between making apps in Rhino. All your JS functions, regardless of which file they are in, if exported and included in app.min.js will be available in App, such as Math.round or any other JS library you know of. Makes things easier, right? Running the application and clicking on the ❓button takes you here. <img class="alignnone wp-image-17708" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b01bf03c93260f451f90f0_js-help-button-in-shiny-rhino-app.webp" alt="JS help button popup alert in Rhino app" width="1200" height="392" /> <h2 id="complete">Complete Shiny app with Rhino🪄</h2> Your Rhino app is now functional, styled, and ready for the world! This was a heavy tutorial with a lot of information in it so let’s recap a few key points. <ul><li>Rhino makes it easier to build an R/Shiny app by managing the app in app/views, which are modules, split logically based on the functionality of the app.</li><li>All the packages, as well as local functions, are imported using box::use().</li><li>On the topic of local functions, all the logic you write goes to app/logic.</li><li>To style things and add interaction, you can use SASS and JS. These can be created in app/styles and app/js.</li><li style="list-style-type: none;"><ul><li style="font-weight: 400;" aria-level="2">Both of them require Node. For SASS, if you do not have Node, you can use the r package by changing the rhino.yml file’s sass listing from node to r.</li><li style="font-weight: 400;" aria-level="2">Both need their partner build_*() functions build_sass() and build_js() to condense them into the app.min.css and app.min.js files.</li></ul> </li> </ul> <ul><li style="list-style-type: none;"><ul><li style="list-style-type: none;"><ul><li style="font-weight: 400;" aria-level="2">You can also use watch = TRUE in the build_*() function calls in a new terminal to start a watch mode for them to avoid calling the function after every minor change.</li></ul> </li> </ul> </li> </ul> And that’s it! These are all the things you need to remember to begin working on your Rhino application. It's a lot to jump right in, but if you forget anything, feel free to explore the Rhino <a href="https://appsilon.github.io/rhino/articles/tutorial/create-your-first-rhino-app.html" target="_blank" rel="noopener">documentation</a>. And if you need assistance with your enterprise project, reach out to our team for help! Oh, and also, don’t forget to have fun! 👋