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 =).
el_cascader(level_inputs =) and el_cascader_panel(level_inputs =)
report each level of the path as an input of its own, under the ids
given -- level_inputs = c("compound", "study", "analysis") gives
input$compound, input$study and input$analysis beside the path in
input$<id>; in a module, ns(c(...)). They follow the path on load,
when the user picks and when update_el_cascader() sets it, and each
notifies its observers only when its own level changes. NULL, the
default, reports the path alone.
The installed package is smaller, 4.3 MB where it was 5.4 MB, with nothing given up a page uses:
el_locales() still
lists all 67.el_page(dev = TRUE)) is in the package's
sources but not in the package built for CRAN, which loads it from the
unpkg CDN.el_form_field() takes options, Element's name for the choices, as
el_select() does. Given before, it went through ... to the select
as its own options prop -- records with no value or label -- and the
dropdown opened onto blank entries; the Forms article's examples wrote
it so. tools/article-shots.R now opens every dropdown of every example
and reports blank options.el_cascader(value =) and update_el_cascader(value =) given a path of
one level -- "c1", with checkStrictly -- sent it as a string rather
than an array, which level_inputs read as no path.on given an event the component handles itself -- change on
el_cascader(), el_select(), el_input(), el_date_picker(),
el_slider() and the other value components, select on el_menu(),
node-click and check on el_tree(), selection-change on
el_table(), 24 in all -- wrote both handlers into one attribute Vue
could not compile, and the component did not mount. Both now run, the
component's own first.use_element()'s example runs as it stands, and el_calendar_events()
has one. Like el_table_data(), it reads the data the server keeps for
a data component; both now read it alike, rather than the calendar's
through the table's.vue_app() to the browser and back, and the Element layer on top of it --
its kinds of component, the events registry, updates and containers.par()
after its plot, and el_locale_dependency()'s example the options() it
sets. The title and description write 'shiny' as the package is named.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.
update*Input() are -- after the outputs the same observers
recalculated -- where they were sent at once: every update_el_*(),
call_el(), update_vue(), call_vue(), el_load_children() and
vue_answer(). A component an observer re-renders and updates now gets
the update once drawn; before, the update reached the component being
replaced and was lost. An update made before a long computation in the
same observer shows after it, as Shiny's do: flush_vue() sends it at
once. update_el_progress() is still sent at once, as withProgress()
is.svEmit* and
svOn* (were elEmit*, elOn*), as they now belong to the Vue layer.flush_vue({ ... }) sends the updates made in it, and those queued
before, at once; flush_vue() alone sends what is queued.el_button(task = TRUE): a task button, as bslib's
input_task_button() -- Element's loading state from the click, set in
the browser, until the server has handled the click, or, bound with
bslib::bind_task_button(), until an ExtendedTask is done. It speaks
bslib's protocol, so bslib's own server code drives it; folded into a
tooltip it works too.conditionalPanel(), a component of this package -- is kept out of
Vue's compiling and moved into place, so v-if and v-for can show and
hide it: it keeps its binding and the user's input, and an output hidden
this way waits, as one in a hidden tab does. Before, it lost its binding
once Vue drew it again, an htmlwidget lost its data, and a component
function in a vue_app() template broke the template.update_vue(insert =, replace =, delete =, at =, key =) changes a list
field in place, by position or by a key field, and update_vue(set =)
sets values by path ("items[3].done"): a long list or a deep object is
not sent whole. The table's and the calendar's insert, replace and
delete are built on it.vue_app(type =, rate =): an input handler and a rate policy for the
value, as Shiny's inputs and el_widget() have.vue_app(events =, on =): events of the template's root forwarded as
input$<id>_<event>, and handlers of one's own -- the machinery Element's
components use, now in the Vue layer for any library.vue_app(use = JS("{ install(app) { ... } }")): a plugin written in R,
the way to reach app.config.$inputs.<id> (another input's value), $errors.<id> (a
data output's error) and $busy (the server working), beside
$recalculating.<id>.setup(), as shinyreact's hooks:
shinyVue.useInput(name, initial), shinyVue.useInputValue(name),
shinyVue.useOutput(id) (its value, recalculating and error) and
shinyVue.useBusy().flush_vue(), task
buttons, changing data in place, what a template can read of Shiny, the
composables, Shiny UI inside a template.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.
events, by Element's name in snake_case -- el_tree(events = "node_drop") reports input$<id>_node_drop. Code reading an event
input that is no longer reported unasked -- input$x_focus,
input$x_visible_change, a dialog's input$x_open -- needs it asked
for.el_on() is gone: el_table(events =) does what it did, the input
always input$<id>_<event> -- a name of one's own was reported outside
a module's namespace.el_tree() takes Element Plus's names: its field map is props = list(label =, children =, disabled =, isLeaf =, class =), as
el_tree_select() and el_tree_v2() take it, where it was
label_field, children_field, disabled_field, is_leaf_field and
class_field; its keys are default_expanded_keys and
default_checked_keys, as el_tree_v2()'s, where they were expanded
and checked -- in update_el_tree() too, which can now change props.
input$<id>_checked is unchanged.el_menu(default_active =), as Element's default-active, was
active; el_upload(data =), Element's data, was extra_data -- the
fields Element's own request sends with action, which Shiny's upload
does not send; input$<id>_close of el_tag() and el_alert(),
Element's close event, was _closed (a dialog's _closed is
Element's closed, after the animation); input$<id>_page_size of
el_pagination(), as its page_size, was _size; and el_tour()'s
open state is visible, as the dialog's, drawer's, tooltip's and
popover's, where it was open. The update_el_*() functions follow.el_events() lists every input a component reports -- its value, its
requests, the events it reports unasked and those it can be asked for --
from one registry, which also checks each component's events (a name
it does not have is an error listing them) and writes each help page's
"Shiny inputs" table.on, on every component: a handler of your own for any event --
Element's, or a DOM event with Vue's modifiers -- as DT's callback and
reactable's onClick let one. on = list("keyup.enter" = JS("function(report, e) { report('enter', e.target.value); }")) reports
input$<id>_enter, namespaced in a module as the id is. An event the
component reports too runs both.list(key, code, ctrl, shift, alt, meta), rather than TRUE.el_tabs() and update_el_tabs() take value as well as selected,
as el_select() does.insert_el_tab() inserts the pane into tabs whose id has a dot or a
colon, as Shiny's ids may: the selector was not escaped.<button>, a select's or an input's <input>.inst/examples/combinations/ and the "Putting
Components Together" article: awkward ids and values (dots and hyphens,
markup and non-ASCII labels, numeric and empty values, 3000 options) and
a long-running app (180 components, insertUI()/removeUI() and
redraws leaving no dropdown behind, a dropped connection). A select in a
space needs a width, as in Element.events, el_events() and on; updates to an output not yet
drawn are dropped, as Shiny's are.update_*() reaches the namespaced id in a module; a
bookmark restores components folded into others and in modules.First release.
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:
Vue.createApp():
one that fails to mount leaves the rest of the page working, and a
component Shiny removes is unmounted.el_input_otp(), el_input_tag(),
el_segmented(), el_select_v2(), el_mention(), el_tree_select(),
el_tree_v2(), el_table_v2(), el_color_picker_panel(),
el_date_picker_panel(), el_check_tag(), el_anchor(), el_tour(),
el_image_viewer(), el_countdown(), el_affix(), el_space(),
el_scrollbar(), el_watermark(), el_text(), el_avatar_group(),
el_splitter(), el_config_provider()."Search"); Element UI's
class names ("el-icon-search") are read as the same icon."large", "default" and "small"; date formats are day.js's
("YYYY-MM-DD"), with Element UI's tokens converted.JS() code is evaluated in the browser wherever Element Plus takes a
value: a VNode built with Vue.h() as a message box's or notification's
message, a space's spacer, a table-v2 column's cellRenderer.el_table_v2(methods =) gives its slot templates functions of your own,
so upstream's row and header renderers port across; auto_resize = TRUE
is Element Plus's el-auto-resizer; update_el_table_v2() replaces its
rows and columns from the server.$setInput(name, value) in any template is Shiny's setInputValue().virtual_ref (tooltip, popover, dropdown) is a CSS selector, looked up in
the browser, including for a target the server draws later; one matching
several elements gives them a single popup that follows the pointer.el_tooltip()'s trigger is Element Plus's own -- how it opens -- and
the element it describes is reference, as for el_popover().
el_popconfirm(popconfirm_width =) sizes the prompt. Popover and
popconfirm take the tooltip's other attributes through ....el_load_children(reject = TRUE) fails a lazy load, so the node can be
loaded again; el_tree_select() loads lazily from the server as
el_tree() does. el_tree(class_field =), el_tree_v2(props =),
el_select(props =), el_select_v2(props =), el_segmented(props =) and
tag_tooltip are new. el_tree_node() names a tree's node for
call_el().el_page(dev = TRUE) loads Vue's development build, which is bundled:
until now it loaded the production build either way, so Vue's warnings
never reached the console the tests read.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.
vue_app() -- a component: template, data, methods, computed,
watch, emits, setup, components and the lifecycle hooks under
Vue's names (multi-word ones also in snake_case), plus id, input (the
field that is input$<id>; several for one value) and use (Vue's
app.use(), any plugin with its options). $emit() of an event in
emits arrives as input$<id>_<event>, several arguments as a list.render_vue_data() -- an output that sends a component's data, not
markup, after shinyreact's reactive_output(): the server renders fields
by name, list(mean = 1, sd = 2), and the components following the
output (vue_app(outputs = "stats"), vue_store(outputs =)) set them,
as update_vue() would -- fields declared in data, as Vue tracks no
others. It waits while its component is hidden, as outputs do, and
$recalculating.<id> tells the template while it runs. One output can
feed several components, or a vue_store() they share. It,
render_vue() and render_el_table() can be cached with bindCache()
and wait for a promise (an ExtendedTask's result); a cached table
output still sends each page only what changed for it.vue_component() -- a child component for components =, under its name
as written or in kebab-case (todo_item is <todo-item>); the
dependencies its template carries come with it.vue_store() -- state shared by components, $store.<id> in every
template, reported to and set from the server on request.vue_output() / render_vue() -- like uiOutput() / renderUI(), but a
render that changes only a component's data updates it in place, keeping
the user's sort, ticks and open tabs. A component given another id is
another component, and renders afresh.update_vue(), call_vue(), vue_answer() -- set fields (value for
the input), run methods, answer a component that asked the server; they
reach setup() state too, and bookmarks restore it.use, so a Vue component of your own on the same page does not.
el_widget()'s report is now input (with emits for the rest);
el_call() is call_el(); update_vue_data() is update_vue().
options(shiny.vue.dev = TRUE) loads Vue's development build, as
shiny.element.dev does.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.
el_call() is call_el(), update_vue_data() is update_vue(), and
el_widget(report =) is input =.insert_el_tab()'s name is tab, which also takes an el_tab_pane().el_table_config() is gone: el_table() takes a data.frame as it is, and
el_table_column() writes the columns.NULL as "leave it", as Shiny's do; NA returns a
prop to Element's default.el_table_output() and
render_el_table(). input$<id>_selection_rows is the selected row
numbers, and input$<id>_selection_change the selected rows, data[rows, , drop = FALSE] of the data shown; input$<id>_selected and
input$<id>_selected_rows are gone. A table reports five of Element's
events unasked -- selection-change, current-change, sort-change,
filter-change, expand-change -- and the others when asked, with
el_table(events =) or el_on().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.
opened.input$<id>_elfile.el_calendar() draws Element Plus's own day cell; it used to fill it with
a template of its own and colour every .is-selected on the page.el_row(justify =, align =) apply without type = "flex", Element UI's
switch, as Element Plus's row is always flex; a guttered row's columns
are marked is-guttered.is-fullscreen, as Element does, where they made it half the width. A
header with a close button is padded clear of it (show-close).el_switch(active_color =, inactive_color =, border_color =) work again,
as Element Plus's CSS variables: it dropped the props.el_card() -- is drawn as
markup, where it showed its HTML source.el_carousel_item(style =, class =): a slide's own height, which a
carousel of height = "auto" takes, as upstream's example sets it.<section>, <header>, <main>,
<aside>, <footer> -- with their sizes as its CSS variables; a side
tab header is laid out in a column, the "+" is Element's, and the active
tab's bar spans its label alone.update_el_*() and call_el() reach it through the
instance that took it in, where they were dropped with a warning.input$el-id-...``.el_select(multiple = TRUE, selected = "a"), el_select_v2(), an update -- is a selection of one; it showed
nothing selected.value_format defaults to
"YYYY-MM-DD HH:mm:ss" for "datetime" and "datetimerange", and the
box shows Element's format for the type (format = NULL), where both
were "YYYY-MM-DD". update_el_date_picker(format =) is new.v-model into a directive Vue could not resolve.el_form_item() puts several fields under one label, in columns, each
with its own rules, as Element's examples nest them. Form fields can be
"select-v2", "segmented", "input-tag", "mention" and
"mention-textarea"; radio and checkbox groups take button = TRUE and
border = TRUE; report = TRUE reports a field as it changes, as
input$<form>_<prop>. update_el_form(label_position =) is new. Radios
and checkboxes take their value as Element Plus's value, not label.button (type,
plain, round left NULL now), link, card (its shadow), dialog
(align_center, draggable, overflow), locale (by code, updatable);
a destroy-on-close dialog inside one keeps its content.update_el_space(),
update_el_splitter(), update_el_splitter_panel(), update_el_text(),
update_el_watermark(), update_el_scrollbar(), update_el_anchor(),
update_el_affix(), update_el_tree_v2(), update_el_avatar_group(),
update_el_button_group(), update_el_config_provider(); and more of
them in update_el_tabs() (position, type, closable, addable, stretch),
update_el_collapse(), update_el_dialog(), update_el_drawer(),
update_el_descriptions() (size, label width), update_el_mention()
(options, so a search can be answered), update_el_skeleton()
(animated, count) and update_el_table() (loading_options).el_table(loading_options =) sets the mask's text, spinner, svg and
background, Element's element-loading-*.class and style on the component itself, as Element passes them to
its root: el_button(), el_card(), el_avatar(), el_image() (whose
fit needs a box), el_menu(), el_skeleton(), el_divider().
el_card(body_style =) takes Element's camelCase names.input$<id>_search of a mention is list(pattern, prefix).id that is not a string is refused with an error: a component given
first to el_result(), el_empty() or el_skeleton() -- whose first
argument is the id -- vanished without one.el_link(type =, underline =) left NULL follow a config provider.shinyjs;renderUI() --
takes its settings and follows their updates; the provider's own
settings no longer read a child's fields of the same name;call_el() on one reached the other;call_el(..., "setChecked")
as after a click.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.
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.
el_input(), el_input_number(), el_select(),
el_radio_group(), el_checkbox_group(), el_switch(), el_slider(),
el_rate(), el_date_picker(), el_time_picker(), el_time_select(),
el_color_picker(), el_cascader(), el_cascader_panel(), el_upload(),
el_autocomplete(), el_transfer().el_table(), el_tag(), el_progress(), el_alert(),
el_badge(), el_card(), el_calendar(), el_tree(), el_timeline(),
el_carousel(), el_icon(), el_link(), el_divider(), el_avatar(),
el_image(), el_tooltip(), el_popover(), el_popconfirm().el_menu(), el_tabs(), el_steps(), el_pagination(),
el_dropdown(), el_breadcrumb(), el_page_header(), el_backtop(),
el_infinite_scroll().el_collapse(), el_dialog(), el_drawer(), el_form(),
el_row(), el_col(), el_container(), el_header(), el_aside(),
el_main(), el_footer().el_message(), el_notification(), el_message_box(),
el_loading(), el_loading_close().el_page(), el_theme(), use_element(),
df_to_tree_data(), df_to_cascader_options(), el_form_validate().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()).
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:
input$tbl_selection_rows, the selected row numbers, integers -- after
Element's getSelectionRows(). It goes into bookmarks, and a bookmarked
selection is ticked again. input$tbl itself is left free: Element's
table has no value of its own.input$tbl_selection_change, the selected rows as R subsets them,
data[rows, , drop = FALSE]: factors, Dates and row names kept, the
input handler subsetting the data the server keeps.input$tbl_current_change, _sort_change, _filter_change and
_expand_change, always; Element's other events -- row-dblclick,
cell-click and the rest -- when asked, with el_table(events =) or
piped, el_table(...) |> el_on("row-dblclick"), under Element's name in
snake_case or an input of one's own (el_on("cell-click", input = "picked")).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.
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/.
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.
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.
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.
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.
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.
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.
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.
shinyjs. hidden() and disabled() wrap a component in the UI,
and click() clicks the button or link inside it, besides hide(),
show(), toggle(), enable(), disable(), reset(), onclick().
Themes while the app runs. Element's brand colours follow
Bootstrap's CSS variables, so session$setCurrentTheme() and
bs_themer() recolour Element's components; the tints are mixed in the
browser, as Sass's mix().
Busy indicators. Under useBusyIndicators() a table output keeps
Element's loading mask in place of Shiny's spinner.
Screen readers. A component an update changes is marked
aria-live, as Shiny marks an input update*Input() changes.
Bookmarking. Every component's value goes through
shiny::restoreInput(), so a bookmarked page reopens as it was left --
inputs, the selected tab, open panels, an open dialog, the pager's page.
Dates. el_date_picker() reports Date for "date", "dates" and
"daterange" with the default value_format, as dateInput() does. A
value_format of your own still reports text in that format.
The session argument. Every server function -- update_el_*(),
el_message(), call_el() and the rest -- takes the current session by
default, as updateTextInput() does. Given an id in the session's place,
update_el_input("name", ...), it says so and names the call to write
instead, as Shiny's own do.
Action buttons. el_button() and el_tag() report their clicks as
actionButton() does: 0 on load, classed so that observeEvent() and
req() treat 0 as not yet clicked. el_dropdown() reports each command
as an event, so choosing the same item twice runs an observer twice.
One input per id. el_cascader() reports to input$<id>, as every
other input does, rather than input$<id>_value, and el_pagination()
reports its page as input$<id> rather than input$<id>_page.
The server loads. Where Element takes a JavaScript function to fetch
data, the server can answer instead: a lazy el_tree(), el_cascader(),
el_cascader_panel() or tree el_table() asks through input$<id>_load
(_lazy_load for a cascader) and el_load_children() replies; a
remote el_select() sends what is typed as input$<id>_query, and
update_el_select() with the matches answers it.
Labels and errors from the server. The update_el_*() of every input
takes label, as updateTextInput() does, and error, Element's
message for a check only the server can make; "" clears it.
Validation. shinyvalidate's messages show on a component as Element
shows a failed form rule: framed in red, the message underneath -- for a
labelled component, under the control, replacing any error it opened
with.
Labels. Every input takes label, shown above it or, with
label_position = "left" or "right", beside it -- also its accessible
name. The props of Element's el-form-item that suit a single input come
with it: label_width, label_suffix, required (the red asterisk),
error, show_message and inline_message; the component's own size
sizes the label. el_upload()'s trigger text is now button_label, as
fileInput()'s buttonLabel.
Element themed from the page's theme. The primary, success,
warning, danger and info of el_page()'s theme reach Element's
components too, with the tints and shades Element derives from each, and
el_theme(element =) sets any of Element's own theme variables --
list("border-radius-base" = "8px"). They are set as Element Plus's CSS
variables, as its theming guide sets them. use_element(theme =) does the same elsewhere; its layout CSS
argument is now layout_css.
A tree filters as it stands. el_tree() has a default
filter_node_method -- the label contains the text, ignoring case -- so
call_el(session, "tree", "filter", list(text)) needs no JavaScript;
Element itself throws without one.
More of upstream. el_checkbox(), one box as checkboxInput() is,
with indeterminate for a "check all" box; el_button_group(), buttons
joined into one bar, each still reporting; an id on el_badge() or
el_link() makes it something the server changes (update_el_badge(),
update_el_link()), the link an action link; el_select(option_template =), Element's custom option template; el_autocomplete(remote = TRUE),
suggestions from the server through input$<id>_query. In forms,
el_rule() takes Element's custom validator and the rest of
async-validator (enum, whitespace, transform), el_form_field()
every control Element's form holds -- "checkbox", "time-select",
"autocomplete", "transfer", "cascader-panel" added -- and
update_el_form() replaces the field list (fields =) or shows the
server's own errors (errors =). An update can carry JS() functions.
Closer to Element's own behaviour. Dialogs and drawers stack, lock
scroll and close on Escape through Element's popup manager, so they share
one z-index counter with every Element popup and honour
el_page(z_index =); opened and closed follow the transitions, and a
drawer gives focus back. Tabs take the arrow keys and Delete and scroll
when they overflow; collapse headers take Enter and Space and animate;
both carry Element's ARIA.
Uploads that fail or are aborted. Files go up one at a time, as
fileInput() sends them; a file that fails, or is stopped with abort(),
is left out and the rest of its batch still arrives. The upload job it
interrupted is let go of at once, with its temporary directory, rather
than kept until the session ends. el_upload_file()
and el_table_row() name a file or a row for a method that needs it.
Server questions are cleaned up. A lazy load the server never answers settles after 30 seconds, and at once when its component is removed or the session ends. A remote select or autocomplete search with no answer stops loading after the same 30 seconds; an autocomplete typed into faster than the server answers shows the answer to the last query, not an earlier one.
Tags where a string was expected. Tags in a component's data --
a timeline entry's content, a column's header_html, a form item's
label_html -- are sent as the HTML they stand for, not as a serialised
object; update_el_*(label =) takes tags or HTML() and draws them as
markup, as update*Input() does. Items, steps and tabs that are not a
list of lists, a gutter that is not a number, and a theme_css that is
not a dependency are refused with a message saying what was expected.
el_cascader() loses its icon argument, which did nothing: Element's
cascader has no icon.
Found while writing the component pages. el_table() columns nest
under group headers (children); el_form_field() has "textarea" and
"password", and its form-item props (required, error, label_width,
...) now reach the form item rather than the control; a wrapper -- a
popover's body, a badge -- takes several components and text side by
side; update_el_tooltip(), update_el_popover(), update_el_popconfirm()
and update_el_infinite_scroll() sent field names the component did not
have and changed nothing -- every updater is now checked against its
component.
Element's global config. el_page() and use_element() take size
and z_index, as Vue.use(Element, {size, zIndex}) does; a labelled
input's label follows the size too. Element's display.css -- the
hidden-xs-only family -- is loaded with the rest, and el gains the
registered components it lacked (button_group, checkbox_button,
scrollbar, spinner, collapse_transition) and loses anchor,
anchor_link and loading, which Element 2 does not have as tags.
Checked arguments. An enumerated argument Element does not accept --
type = "primry" -- is an error listing the values it does, rather than
a component drawn in its default style.
Templates. Each component's template travels as a script the browser does not parse: nothing flashes before Vue runs, and camelCase attribute names reach Vue unchanged.
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.
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}"
)
))
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".
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.
inst/element-plus/ and Vue in inst/vue3/
rather than loaded from a CDN, so apps work offline.el_page(dev = TRUE) loads Vue's development build, which surfaces template
warnings in the browser console.