shiny.element 0.3.1

A smaller package, ready for CRAN, and the fixes and additions since 0.3.0: each level of a cascader’s path as an input, on on any event, el_form_field(options =).

New features

Bug fixes

Documentation

shiny.element 0.3.0

The Vue layer, reviewed against vueR, vuer, reactR, shiny.react and shinyreact for three things: all of Vue, all of Shiny and shinyjs, and libraries other than Element built on it.

Breaking changes

New features

Documentation

shiny.element 0.2.0

Breaking changes

Not yet on CRAN, so the API may still move without a deprecation; from the first CRAN release on, a change like these goes through one first.

New features

Bug fixes

Documentation

shiny.element 0.1.0

First release.

Element Plus

Built on Element Plus 2.14.7 and Vue 3.5, bundled; the development versions built on Element UI 2.15.14 and Vue 2.7 are tagged v0.1.0-vue2. The “Migration from Element UI” article lists what changed for code written for them. In short:

What Element Plus writes in JavaScript, from R

A Vue layer, usable on its own

The bridge every component is built on is exported, and knows nothing of Element: write a Vue 3 component in R with Vue’s own options and use it as a Shiny input.

Breaking changes

Before this first release the API is still allowed to move; from the first CRAN release on, a change like these goes through a deprecation first.

Items as functions

The parts Element Plus writes as child tags have constructors, as bslib has nav_panel() and accordion_panel(): el_tab_pane(), el_collapse_item(), el_timeline_item(), el_descriptions_item(), el_carousel_item(), el_step(), el_breadcrumb_item(), el_dropdown_item(), el_tour_step(), el_anchor_link(), el_menu_item(), el_sub_menu(), el_menu_item_group(), el_option(), el_option_group(), el_table_column(), el_table_v2_column() and el_skeleton_item(). Each takes the child’s own attributes as arguments – documented, completed by the editor, checked – and returns the item its parent’s argument takes, so el_tabs(tabs = list(el_tab_pane("One", ...))) and the plain list it replaces draw the same, and update functions take items too. A timeline entry’s hide_timestamp is new. insert_el_tab() takes an el_tab_pane(), as bslib’s nav_insert() takes a nav_panel(); its name argument is now tab.

Behaviour closer to Element Plus

Documentation, after Element’s own

The website follows Element’s documentation: a page per component, in Element Plus’s groups and order (Basic, Configuration, Form, Data, Navigation, Feedback, Others), each with Element’s demos written in R – live where they need no server, a screenshot of the running app where they do – and Element’s API tables with where each attribute, event, method and slot is in R. The guides cover what Element’s documentation does not: forms and validation, Shiny integration (events, methods, modules, bookmarking, data from the server, components of your own), theming and languages, and what differs from Element in a browser.

Each demo is upstream’s whole: its controls too – the radio buttons that resize, the switches that toggle, the buttons that open a preview – as a Shiny app whose server does what upstream’s script does, each checked in a browser (shot_expect in tools/article-shots.R) for doing it.

Components

Every component Element Plus 2.14.7 documents is wrapped, services included, with every documented attribute, event and slot reachable from R and every method callable by name – measured by tools/api-coverage, not counted by hand. Tabs, collapse, dialog and drawer are reimplemented as markup so the components inside them stay connected; the “What works” article lists where they differ from Element’s own.

Most components have a matching update_el_*() for server-side updates, and report their value through input$<id> like any other Shiny input. As with Shiny’s own update*Input(), a value set from the server is reported back through input$<id> too.

el_page() themes the page around the components with el_theme(), a bslib theme carrying Element’s colours, font and control sizes, so Shiny’s own inputs and outputs match the Element ones beside them. theme = NULL leaves Shiny’s plain Bootstrap. Bootstrap’s dark mode – bslib’s input_dark_mode() – turns Element Plus’s with it, and bslib’s tooltip() and popover() take a component as their trigger.

Events that fire on every frame – a scroll, a slider or splitter drag, a tree node dragged over, a colour picked in the panel – reach the server at most every 200 ms, the last one always, so it hears where the scroll or the drag ended.

Tabs can be added and removed from the server (insert_el_tab(), remove_el_tab()) or by the user (editable = TRUE), and a lazy pane binds its content when first shown. Messages and notifications given an id can be closed by it (el_message_close(), el_notification_close()).

Tables

A table is an output in an app, as DT’s and reactable’s are: el_table_output("tbl") in the UI, output$tbl <- render_el_table(el_table(data = ...)) in the server. Rendered again, it is patched in place: the same rows keep the user’s ticks, sort and open rows. Its inputs are named after the output:

el_table_column(editable =) edits a column’s cells in place, in Element’s input, input-number, select or date picker: a double click opens the editor, Enter or leaving it commits, Escape abandons, Tab moves to the next editable cell. The edit is shown at once, applied to the server’s copy of the data, and reported as input$<id>_cell_edit, list(row, column, value, old) with the column’s type. Nothing Excel does beyond that – ranges, copy and paste, fill, undo – is attempted.

A table output sends its markup once: rendered again with the same columns and options, it sends only the data that changed, as JSON. While Shiny recalculates it, Element’s loading mask covers it in place of Shiny’s fading (el_table_output(loading = FALSE) for the fading).

update_el_table(insert =, replace =, delete =) changes a few rows and sends only those; the server’s copy of the data changes as R would change it, and el_table_data() reads it, reactively. Rows not touched keep their ticks and open state.

el_table() returns the table’s specification, drawn when placed, as an htmlwidget is; without Shiny – R Markdown, Quarto, the site – it is placed as it is, with no id.

el_table() columns may carry a cell template, drawn once per row with scope.row in reach – a status tag, a progress bar, a column of buttons – and a type of "index" or "expand". A button in a cell reports with rowAction('edit', scope), setting input$<id>_edit to the row’s number and the row. loading shows Element’s loading mask, from the server too.

el_table() draws no column borders by default, as Element does; pass border = TRUE for them.

update_el_table(data =) keeps the columns the table was created with. It used to re-infer them from the new data, discarding every label, formatter and template.

A column’s header is a template, as its cell is: a search box, a button, any component in the header cell. Group headers nest as deep as the columns given (they stopped at two levels below the top).

Every update_el_*() takes each argument of its component that can change once it is drawn, under the same name – update_el_table(stripe =, table_layout =), update_el_select(multiple =, filterable =), update_el_input(maxlength =), about 900 in all – each documented on its help page, inherited from the component’s. NULL leaves one as it is, as in Shiny’s updaters; NA returns it to Element’s default. A test keeps every argument held in a component’s data reachable this way. The containers drawn as markup (tabs, collapse, dialog, drawer) and the wrappers (tooltip, popover) keep their own updaters. Each update function is documented on its component’s page, as bslib documents update_switch() with input_switch(): one argument, one description. call_el() reaches a table-v2 with auto_resize = TRUE; it used to reach the resizer around it.

A data.frame inside the data – a row’s own rows, a cell of a list column – is rows too, and a list column’s cell is its value rather than a list of one.

The table, table-v2 and calendar pages show Element Plus’s demos with Element Plus’s data; where a demo changes the component as it runs, the R version is an app whose server does it.

Dashboards

A new article builds whole apps: Element’s own admin-layout example rebuilt in R, a sales overview, and an orders admin page with search, row actions, server-side sorting and paging, an editing dialog and confirmed deletes. Each is also in the installed package, under examples/dashboards/.

Shiny modules

UI functions no longer namespace their id from the reactive domain they are built in; inside a module, wrap the id in ns() as for any Shiny input. Built by renderUI() in a module’s server, a component used to be namespaced twice – ns("x") became "mod-mod-x" – and never reported. Their session argument is deprecated; a session passed to it still namespaces the id, with a warning. update_el_*() and the other server functions are unchanged: they namespace the bare id, as update*Input() does.

Without Shiny

Components work on a page with no Shiny behind it – R Markdown, Quarto, a saved HTML file: they render and respond to the user, with nowhere to report to. Every call into Shiny is guarded, the tabs, collapse, dialog and drawer bind themselves when Shiny is absent, and the scripts bring their own jQuery. The website’s examples are live components built this way, with a screenshot only where an example needs a server.

On any page

Every component carries Vue and Element Plus, as Shiny’s inputs carry selectize: on fluidPage(), navbarPage() or a bslib page with no use_element() they used to stay bare <el-*> tags. use_element() and el_page() now give what belongs to the page – the theme Element follows, its locale and global config, the feedback handlers, Element from the CDN. Should Element still be missing – a CDN that cannot be reached – the console says so rather than blaming the tags’ place.

Components rendered by renderUI()

A component whose type first appears through renderUI() or insertUI() now hears update_el_*() and call_el(). Their handlers registered only on shiny:connected, which had already fired by the time such a component’s script arrived, so every update to it went nowhere without a word.

Raw Element tags

el$ tags are documented for what they are: markup for inside a component – el_widget(), template(), slots, table cells, a wrapper’s trigger. At the top level of a page nothing compiles them; the browser console now says so instead of leaving bare text.

A calendar of events

el_calendar(events =) draws each day’s events as Element’s tags: a date, as Element names a cell’s day, optionally an end for a span, a title, a body shown on hover, an id – toastui’s names – and Element’s tag type or a color of its own; a title left empty draws a block of colour. In an app whose events the server reads, the calendar is an output, as toastui’s is: el_calendar_output() and render_el_calendar(), rendered again by patching – only the events sent, the month the user went to kept – cacheable with bindCache(), a promise waited for. el_calendar_events() reads the events it shows.

With editable = TRUE the user double-clicks a day to add an event, clicks one to edit or delete it in Element’s dialog, and drags it to another day. As toastui’s calendar, the server owns the events: each action arrives as a request – input$<id>_add, _update (list(event, changes), toastui’s shape), _delete – with its days as Dates, and the server answers by changing its data or with update_el_calendar(insert =, replace =, delete =), by id. _click reports a click on an event and _dates the days drawn. Without Shiny the calendar applies changes itself. The dialog’s words come from event_labels; a day cell of one’s own can call eventsOn(data.day).

What toastui’s month view has, Element’s pieces draw: a date or end with a time ("YYYY-MM-DD HH:MM", a date-time) shows it before the title, all-day events first – toastui’s category = "allday" keeps a day – and comes back as a date-time; the dialog has an all-day switch and the body. calendars groups events by calendarId, colours them and hides a group with isVisible = FALSE; visible_event_count puts a day’s other events behind “+N more”; use_detail_popup shows an event the user cannot edit in a popover; dragging across days adds an event over them; isReadOnly keeps one event as it is; first_day_of_week (1 to 7, as Element UI’s firstDayOfWeek) and workweek change the week. Where toastui’s popups are fixed, slots take Element’s components: event for the tag, eventForm for the dialog’s fields (a form.<field> travels with the event), eventDetail for the popover.

Every component is a Shiny input

Each component is a host element carrying its id, with Element’s markup inside and a Shiny input binding on it – the way reactR binds React components – so the rest of Shiny reaches it as it reaches textInput(): shinyjs::hide(), show(), toggle(), disable(), enable() and reset(), removeUI() (which destroys its Vue instance too), bookmarking, shinyvalidate, a test driver’s set_inputs(), and bslib’s containers – a sidebar, a card, a closed accordion, a nav panel not yet shown. Vue 3, the version Element Plus runs on, is bundled beside Element Plus, and JS() marks JavaScript the way htmlwidgets::JS() does, without depending on htmlwidgets. (Development versions built on vueR’s htmlwidgets, where the id sat on a hidden element beside the component and Shiny did not know it was an input.)

el_widget() builds the same shape for components of your own, and its new report argument names the value – an input made of el$ tags, as the Shiny integration article shows, needs no JavaScript.

Shiny conventions

Note for users of the development version

el_table() now takes id first, like every other component:

el_table("my_table", data = df)     # new
el_table(data = df, id = "my_table")  # also fine, and always was

Positional calls written against the old el_table(data, columns, id) order still work – the arguments are shifted back with a warning – but naming them is the way to keep it quiet.

input$<cascader id>_value is now input$<cascader id>, and input$<pager id>_page is input$<pager id>. update_vue_component() and vue_handler_dependency() are gone: update_vue_data() does the first’s job, and the bridge loads with every component. use_element()’s first argument is now the page’s theme; its layout CSS is layout_css.

Filling a slot

Every component takes slots, a named list, one entry per Element slot:

el_alert("a", slots = list(title = tags$b("Something went wrong")))

# A component works as slot content too, and keeps reporting its inputs
el_alert("a", slots = list(title = el_tag("sev", "critical", type = "danger")))

A scoped slot, where Element hands the template its own data, is written with template():

el_calendar("cal", slots = list(
  dateCell = template(
    htmltools::HTML("<p>{{ data.day.slice(8) }}</p>"),
    slot = "dateCell", scope = "{date, data}"
  )
))

Reaching a component’s methods

Element documents methods as well as props. call_el() invokes one:

observeEvent(input$clear, {
  call_el(session, "tbl", "clearSelection")
})

# A method with a return value answers asynchronously
observeEvent(input$ask, {
  call_el(session, "tree", "getCheckedKeys")
})
observeEvent(input$tree_get_checked_keys, {
  message("checked: ", paste(input$tree_get_checked_keys, collapse = ", "))
})

The answer arrives as input$<id>_<method> with the method name in snake_case. Each component’s help page lists what it accepts under “Element methods”.

English by default

el_page() and use_element() load English for Element Plus’s built-in text – placeholders, empty-table messages, date-picker buttons. All 67 of its locales are bundled (el_locales()), and options(shiny.element.locale = "zh-cn") sets one for a whole session.

Design notes