Auto Init starts all of the registered Expressive components with a single call.
The IIFE bundle exposes it as Expressive.AutoInit.
Call AutoInit() yourself after the DOM is ready. Importing the JavaScript
installs a few document-level behaviors (Forms, Chips, Slider, Cards, ExpandingCard,
Dialogs, BottomSheets, and SideSheets), but it does
not call AutoInit() for you.
This documentation site calls it in docs.js on DOMContentLoaded.
Initialization
document.addEventListener('DOMContentLoaded', function() {
Expressive.AutoInit();
});
Or pass a root element and per-component options. The option keys are the component names from the table below.
Expressive.AutoInit(document.body, {
Menu: {
// pass options for Menu here
},
FloatingActionButton: {
// pass options for FloatingActionButton here
}
});
From the module build:
import { AutoInit, Tooltip } from './js/expressive.mjs';
AutoInit();
const element = document.querySelector('.custom-tooltip');
Tooltip.init(element, { position: 'top' });
Component init() methods also accept a NodeList or HTMLCollection and return an array of instances. An empty collection returns an empty array.
Options
AutoInit(context, options, settings) takes three arguments:
| Name | Type | Default | Description |
|---|---|---|---|
context |
Element | document.body |
DOM element or shadow root to search within for components. |
options |
Object | {} |
Options for each component, keyed by name. See that component's page for its own options. |
settings |
Object | {} |
{ watch: true } keeps starting components in markup added to context later. See Markup added later. |
You can call AutoInit() again on the same elements. Each component's constructor
destroys any existing instance on that element before creating a new one.
Options in markup
To give an element its own options for AutoInit(), add a
data-name-options attribute that holds a JSON object. The name is
the component's name from the table below in kebab case. The keys are its option
names as written in script.
<a class="button menu-trigger" data-target="actions"
data-menu-options='{"closeOnClick": false, "placement": "top"}'>Actions</a>
<a class="button tooltipped" data-tooltip="Saved"
data-tooltip-options='{"enterDelay": 300}'>Save</a>
<select data-form-select-options='{"classes": "wide"}'>...</select>
-
Menureadsdata-menu-options,FormSelectreadsdata-form-select-options, andFloatingActionButtonreadsdata-floating-action-button-options. The name comes from the table, not the class, so a.date-pickerreadsdata-datepicker-options. An element that is both a tooltip and a menu trigger reads each attribute for its own component. -
Options passed to
AutoInit(context, options)win for the keys they set. A nested object such asi18ncounts as one key. Tooltip reads its owndata-tooltip,data-tooltip-idanddata-positionagain each time it opens, so those win over both. Set the position, and Menu'scontextMenu(data-context-menu), in one place. -
Markup can set an option whose default is a string, number, boolean, array or plain
object, with a value of the same kind.
AutoInit()checks keys inside an object such asi18nthe same way. If it rejects one, the whole object keeps its default. An object whose default is empty, such as FormSelect'smenuOptions, accepts no keys from markup. When the default is a non-empty array, the items need the kind of its first item. Callbacks, elements and options whose default isnullcannot come from markup; pass those from script. Autocomplete'sallowUnsafeHTMLand thedatalists of Chips and Autocomplete are script-only too. -
AutoInit()treats the attribute as untrusted. It skips invalid JSON, an unknown key or a value of the wrong kind with a console warning, and that option keeps its default. A value that passes these checks but still makes a component throw stopsAutoInit(), as the same value passed from script would. -
Only
AutoInit()reads the attribute. A directinit()call uses the options it is given. Menus inside a.menubartake their options from Menubar'smenuOptions, sodata-menu-optionshas no effect on them.
Markup added later
AutoInit() scans context once. To keep watching it, pass
{ watch: true } as the third argument. Components then start in markup added
later, and AutoInit() destroys them when their markup is removed. The call returns
a function that stops watching.
const stop = Expressive.AutoInit(document.body, {}, { watch: true });
// Starts a Menu once the observer sees the trigger, without calling init.
document.querySelector('main').insertAdjacentHTML('beforeend', `
<a class="button menu-trigger" data-target="more">More</a>
<menu id="more"><li><a href="#!">Share</a></li></menu>`);
// In your view's teardown. Components already started keep running.
function unmount() {
stop();
}
-
Added markup starts the same way the first scan does, with the same
options,data-name-optionsattributes andno-autoinitopt-out. The added element is checked as well as its descendants. Components start in a later microtask, when the browser reports the change, not during the call that adds the markup. - An element that already has an instance keeps it. Starting a component yourself before adding its element works, and components that build their own markup, such as the Datepicker's month and year selects, keep the instances they made.
-
Removed markup destroys the instances
AutoInit()would have started. Elements withno-autoinitare left to your code. An element moved elsewhere in the page keeps its instance. One that is removed and added back later starts again. -
Only added and removed elements count. Adding
tooltippedto an element already in the page does not start a Tooltip; callinitfor that. -
Markup inserted at runtime becomes live components. If you insert user content, sanitize
it first and drop component classes and
data-name-optionsattributes you don't want started. -
Watch mode does not enter shadow roots. To watch markup inside one, call
AutoInit(shadowRoot, options, { watch: true })for that root. -
Call it once per root. A component that throws while starting stops the rest of that
batch, the same way it stops
AutoInit(). The watcher still handles later changes.
stop() first handles changes the browser has not reported yet, so markup added
just before the call still starts. Without watch, AutoInit()
returns a function that does nothing.
Components
These are the components AutoInit() starts, and the selector each one claims.
Elements that also have no-autoinit are skipped.
| Name | Selector |
|---|---|
AppBar | header.medium, header.large, header:has(> :is(nav:not(.tabs, .navigation-bar, .navigation-rail), .bar) > .search-bar) |
Autocomplete | .autocomplete |
ButtonGroup | .button-group |
Cards | article:has(> aside[id]:not([id=""])):not(:has(> aside ~ aside)):has(> button.card-reveal-trigger[type="button"], > :not(aside) button.card-reveal-trigger[type="button"]) |
ExpandingCard | article.expanding-card:has(> dialog.expanding-card-dialog) |
Carousel | .carousel |
Chips | .chips |
Confetti | .confetti |
Particles | canvas.particles |
Keypad | .keypad, .keyboard |
ColorPicker | .color-picker |
BarChart | .bar-chart |
PieChart | .pie-chart |
HeatmapChart | .heatmap-chart |
RadarChart | .radar-chart |
RadialChart | .radial-chart |
MixedChart | .mixed-chart |
ScatterChart | .scatter-chart |
FunnelChart | .funnel-chart |
SankeyChart | .sankey-chart |
TreemapChart | .treemap-chart |
ColumnChart | .column-chart |
CommandPalette | dialog.command-palette |
Questionnaire | form.questionnaire |
Datepicker | .date-picker |
LineChart | .line-chart |
Menu | .menu-trigger |
MessageScroller | .message-scroller |
PullToRefresh | .pull-to-refresh |
LoadingOverlay | .loading-overlay |
SwipeActions | .list.swipeable |
InputMask | input[data-mask] of type text, tel, search, url or password |
ShowMore | .show-more |
CurrencyInput | input[data-currency] of type text |
Lightbox | .lightboxed |
NavigationRail | .navigation-rail |
ScrollSpy | .scrollspy |
BackToTop | .back-to-top |
FormSelect | select without data-tree |
TreeSelect | select[data-tree] |
Cascader | .cascader |
Tabs | .tabs |
Timepicker | .time-picker |
Tooltip | .tooltipped |
HoverCard | .hover-card-trigger |
FloatingActionButton | .fab-menu |
Menubar | .menubar |
Snackbar, CharacterCounter, and Slider stay out of this table. Slider still starts itself when the
bundle loads. Forms, Chips, Cards, ExpandingCard, Dialogs, BottomSheets, and SideSheets also run an
import-time Init(). Chips, Cards, and ExpandingCard appear in the table as well so a
later AutoInit() can pick up elements added after load.
Ignoring elements
To make AutoInit() skip an element, add the class
no-autoinit. Initialize that element yourself with the component's
init method when you need options other than the defaults.
<a class="button tooltipped no-autoinit" data-tooltip="Hi">Hover</a>
Expressive.Tooltip.init(
document.querySelector('.tooltipped.no-autoinit'),
{ position: 'top' }
);
Events
Menu, Lightbox, NavigationRail, Cards and ExpandingCard fire DOM events from the
element they were initialized on. expressive:open and
expressive:close fire when the state changes, and
expressive:opened and expressive:closed when the motion
ends. Cards and ExpandingCard fire only expressive:open and
expressive:close.
Each event comes right after the matching on… callback, which still
works. The events bubble and cross shadow roots, so one listener on the document
sees every component. Events from nested components bubble too, so check event.target.
Closing a Menu or Lightbox from its expressive:open listener stops it from opening.
document.addEventListener('expressive:open', (event) => {
if (event.target.matches('.menu-trigger')) analytics.track('menu-open');
});
Choices and changes carry what was chosen in event.detail:
| Event | Fired by | detail |
|---|---|---|
expressive:change | Tabs, Carousel | { panel }, { item, dragged } |
expressive:select | Menu, Datepicker, Timepicker | { item }, { date }, { hours, minutes } |
expressive:confirm, expressive:cancel | Datepicker, Timepicker | none |
expressive:autocomplete | Autocomplete | { values } |
expressive:create | Autocomplete, from its "Add" row | { text } |
expressive:chip-add, chip-select, chip-delete | Chips | { chip } |
expressive:change | ButtonGroup, Menu checkbox and radio items, ScrollSpy (from the section), Tour | { button, pressed }, { item, checked }, { link }, { step, index } |
expressive:open, expressive:close | Tooltip, HoverCard, Snackbar, Tour, ShowMore | Tour's close: { completed } |
expressive:action, expressive:closed | Snackbar | none |
expressive:copy | Copy buttons | { text, copied } |
expressive:refresh | Pull-to-refresh | { waitUntil } |
expressive:swipetoggle | Swipe actions | { row, open } |
expressive:transfer | Transfer list, from the container | { rows, from, to } |
expressive:change, expressive:sort | Data tables, from the wrapper | { selected }, { header, column, direction }; sort is cancelable |
An Autocomplete or Select input hosts a Menu, so it also fires that Menu's
expressive:open, expressive:close and
expressive:select. A callback that changes the choice again, such as
an onShow that selects another tab, leaves only the event for the
final choice.
Tooltip fires expressive:open and expressive:close when it
shows and hides, after its delays, and so does a hover card, from its link. A snackbar fires its events from the
snackbar itself. expressive:closed comes just before it leaves the page.
Tour's expressive:close says whether the Done button finished it.
Alpine.js
AutoInit() scans the page once, so it misses anything Alpine renders later with
x-if or x-for. Watch mode starts and destroys that
markup with the options of the AutoInit() call and its data-name-options
attributes. To pass options from Alpine expressions instead, register a directive. Alpine runs it each time it
creates the element and calls cleanup when it removes it, so every instance gets
destroyed with its element.
document.addEventListener('alpine:init', () => {
Alpine.directive('expressive', (el, { value, expression }, { evaluate, cleanup }) => {
// HTML lowercases attribute names, so match the export name the same way.
const name = Object.keys(Expressive).find((key) => key.toLowerCase() === value);
const instance = Expressive[name].init(el, expression ? evaluate(expression) : {});
cleanup(() => instance.destroy());
});
});
Write the component name after the colon and pass options as the expression. Add
no-autoinit so a later AutoInit() call leaves the element alone.
<a class="button tooltipped no-autoinit" data-tooltip="Hi"
x-expressive:tooltip="{ position: 'top' }">Hover</a>
Select, Autocomplete and Datepicker fire a native change event when the user picks a
value, so x-model picks it up. When your code sets the value, the visible field stays
stale until you call refresh(). Do the same after adding or removing options.
Put the select in its own .field so the template root stays the element Alpine created.
<template x-if="open">
<div class="field">
<select id="letter" class="no-autoinit" x-model="letter" x-expressive:formselect
x-effect="letter; $nextTick(() => $el.Expressive_FormSelect?.refresh())">
<option value="a">Alpha</option>
<option value="b">Beta</option>
</select>
<label for="letter">Letter</label>
</div>
</template>
Component events bubble, so Alpine listens to them like any other:
@expressive:change="tab = $event.detail.panel.id". Dialogs and sheets are native
<dialog> elements. Open one with $refs.dialog.showModal().