@use "/shell" as *; @use "quark:list" as list; #release-notice { /* opens once: sheet re-runs restart a delay, so gate it on the fact it writes */ &:not([data-did-open]) { @delay 7000 { is-open: ""; data-did-open: ""; } } /* prevent clicks from bubbling to the sheet */ @on mouseup, click (stop-propagation); } provider-fetch[api-url*="package-metas/index.json"][is-success] { $package-indices: prop("provision").body; /* `excom.navGroup` moves a package out of its type's list, into that group */ $ungrouped: list.reject($package-indices.packages, "navGroup"); .package-links { [bind-elements] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "kit-element")); } [bind-element-bases] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "element-base")); } [bind-tools] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "tool")); } /* the standalone libraries: same rows, rendered flat (shell.css) */ [bind-libraries] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "library")); } [bind-library-group] ul:not(ul ul) { content: iterate(list.filter($package-indices.packages, "navGroup", "libraries")); } ul:not(ul ul) > li { $pkg: item.shortName; > spa-a { route-href: "/packages/#{item.shortName}"; } } /* one `content` rule per link: a second one would rewrite the first's text on every pass */ ul:not(ul ul, [bind-libraries] ul) > li > spa-a { content: item.shortName; } [bind-libraries] ul:not(ul ul) > li > spa-a { content: displayName(item.shortName); } /* packages whose docs span several pages: one collapsible group per section */ [bind-sections] { content: iterate(item.docSections); summary { content: item.title; } details > ul { content: iterate(item.docs); spa-a { route-href: "/packages/#{$pkg}/#{item.name}"; content: item.title; } } details:not(:has(spa-a[is-active])) { open: none; } } } /* page titles: the page, then the site. The docs home keeps the title in the document head; the other routes carry theirs in markup */ spa-route[is-active] { /* a site guide */ &[route-href$=":name"] { $title-guide: list.find($package-indices.docs, "name", $route.params.name); document-title: "#{$title-guide.title or $route.params.name} · Nucleus · docs"; } /* a package README */ &[route-href$=":packageName"] { document-title: "#{displayName($route.params.packageName)} · Nucleus · docs"; } /* one page of a package's docs */ &[route-href$=":docName"] { $title-package: list.find($package-indices.packages, "shortName", $route.params.packageName); document-title: "#{docTitle($title-package, $route.params.docName)} · #{displayName($route.params.packageName)} · Nucleus · docs"; } } /* the page's markdown file, for the footer link: the docs home and each guide the index lists, and a package's page whose index entry says `markdown` (build-docs-index sets it; the dev server's index has none, so no link there). siteDocHref and the path check say which URL is the page's own: the 404 and a package's doc pages have none */ > spa-manager[active-url] { $page-path: attr("active-url").split("#").at(0).split("?").at(0); $page-name: if($page-path == SITE_HOME: SITE_HOME_DOC; else: $page-path.split("/").at(-1)); $page-guide: list.find($package-indices.docs, "name", $page-name); $page-package: list.find($package-indices.packages, "shortName", $page-name); $page-markdown: if($page-guide and siteDocHref($page-guide.name) == $page-path: "/docs/#{$page-guide.name}.md"; $page-package.markdown and "#{SITE_BASE}/packages/#{$page-package.shortName}" == $page-path: "/#{$page-package.shortName}.md"); footer [bind-page-markdown] { href: $page-markdown; content: ternary($page-markdown, "Markdown version of this page"); } } } spa-route { $route: prop("provision"); /* paramless routes name their guide in markup (the docs home, `/`); /docs/:name gets it from params */ $route-doc-name: attr("data-doc-name"); } /* the desktop aside and the mobile sheet stamp the same nav template */ [data-site-nav] { details:has(spa-a[is-active]) { /* not kosher */ open: ""; } spa-a[is-active] { aria-current: ""; } spa-a:not([is-active]) { aria-current: none; } } /* mobile sheet: Escape / back gesture while open; a tapped link closes it */ #site-menu { &[is-open] dismiss-watcher { is-active: ""; } &:not([is-open]) dismiss-watcher { is-active: none; } @on click (target: "spa-a") { is-open: none; } } #site-menu-gesture { &:has(> #site-menu[is-open]) { progress-offset: 1; } &:not(:has(> #site-menu[is-open])) { progress-offset: 0; } @on gesture-handler-start { #site-menu { is-scrubbing: ""; } } @on gesture-handler-end { #site-menu { is-open: event.detail.snap == 1; is-scrubbing: none; } } } main { @on copy-source (handle: copySource); } #search-dialog[open] { > include-content { is-active: ""; /* for some reason this is necessary on first render */ @on include-content-did-render (handle: focusInput); } input[type="search"] { /* works on all subsequent opens */ autofocus: ""; } } #search-dialog:not([open]) input[type="search"] { autofocus: none; }

event-handler

Stitch behaviors of your app together, primarily through events.

Features

  • Fire custom events Map any input event, such as clicks, or lifecycles to custom output events
  • Commands command-name invokes the HTML Command API — built-in verbs (show-modal) and the --verb commands elements accept (--submit, --close)
  • Retarget Aim events/commands at any selector (target-ref) — where a <button commandfor> needs an id
  • Global / host listening Listen to events globally or on any element. Helpful for: escape to dismiss, global shortcuts, etc.
  • Keycode filter Escape to dismiss, Shift+K shortcuts — keys / modifier chords
  • Listen filters Debounce, selector, and pathname gates
  • Custom Event Payloads detail-* attributes and forms convert to event.detail JSON

In a view that already has a <quark-sheet>, the same wiring is a rule: @on click (target: "[data-add]") { @dispatch cart-add (detail: (sku: attr("data-sku"))); } — @on options cover selector-filter / keycode-filter / is-debounced / host-ref, and @dispatch / @command cover fire-event / target-ref / form-ref / command-name. Keep <event-handler> for markup without a sheet.

Installation

This package is available in the NucleusKit. Or it can be used by itself:

CDN Package Manager
<script src="https://unpkg.com/@excom/kit-utils@0.3.0/dist/index.umd.min.js"></script>
<script src="https://unpkg.com/@excom/neutron@0.2.0/dist/index.umd.min.js"></script>
<script src="https://unpkg.com/@excom/event-handler@0.1.4/dist/index.umd.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@excom/event-handler@0.1.4/dist/index.css">
npm install @excom/event-handler
HTML Imports JS / CSS Imports
<!-- import path to `node_modules` will depend on your build setup -->
<script type="module" src="/node_modules/@excom/event-handler"></script>
<link rel="stylesheet" href="/node_modules/@excom/event-handler">
import "@excom/event-handler";
@import "@excom/event-handler/index.css";
Peer dependencies (0)

Packages a consumer must install alongside this one. Workspace deps are bundled.

Package Version
View Dist Files

All exports:

.
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./event-handler
  • types: ./dist/event-handler.d.ts
  • import: ./dist/event-handler.js
  • default: ./dist/event-handler.js
./event-handler.js
  • types: ./dist/event-handler.d.ts
  • import: ./dist/event-handler.js
  • default: ./dist/event-handler.js
./event-handler.min
  • types: ./dist/event-handler.d.ts
  • import: ./dist/event-handler.min.js
  • default: ./dist/event-handler.min.js
./event-handler.min.js
  • types: ./dist/event-handler.d.ts
  • import: ./dist/event-handler.min.js
  • default: ./dist/event-handler.min.js
./event-handler.umd.min
  • types: ./dist/event-handler.d.ts
  • default: ./dist/event-handler.umd.min.js
./event-handler.umd.min.js
  • types: ./dist/event-handler.d.ts
  • default: ./dist/event-handler.umd.min.js
./index
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index.bundle.min.css
  • default: ./dist/index.bundle.min.css
./index.css
  • default: ./dist/index.css
./index.js
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index.min
  • types: ./dist/index.d.ts
  • import: ./dist/index.min.js
  • default: ./dist/index.min.js
./index.min.css
  • default: ./dist/index.min.css
./index.min.js
  • types: ./dist/index.d.ts
  • import: ./dist/index.min.js
  • default: ./dist/index.min.js
./index.umd.min
  • types: ./dist/index.d.ts
  • default: ./dist/index.umd.min.js
./index.umd.min.js
  • types: ./dist/index.d.ts
  • default: ./dist/index.umd.min.js

Usage

Listen (default click), then either fire-event or command-name.

<event-handler fire-event="cart-add">
  Add to cart
</event-handler>

Or, for example: a successful form submit re-fetches the related data by invoking the provider's --fetch command.

<event-handler listen-for="super-form-success" target-ref="#fetch-todos" command-name="--fetch">
  <super-form>
    <form>
      <!-- form to create a new todo -->
    </form>
  </super-form>
</event-handler>

In a Nucleus Stack application, this will be one of the most heavily used elements. It is the primary method of linking a functional cause and effect.

API Reference

event-handler

Attributes (21) Role — option is configurable, state is managed by the element (read-only), or hybrid which is both. Role Attribute Type Values Prop Default option command-name
Space-separated commands to invoke on the target: custom --verb commands (--submit, --close) dispatch a command event, as a <button command commandfor> would; built-in verbs (show-modal, close, toggle-popover) run through the platform. commandName
tokenlist <command>… null
option delay-ms
Delay handling by this many milliseconds. delayMs listenable-element
number null
option fire-event
Space-separated event names to dispatch on the target. Each name gets the same merged detail. Ignored when mutate-target is set. Pair with detail-* attributes for static detail fields. fireEvent
tokenlist <EventName>… null
option form-ref
<form> whose fields merge into event detail, or become attributes when mutate-target is set. formRef
string <CSS Selector> null
option host-ref
Listen on another element / window / document — e.g. Escape to dismiss a dialog from a global keydown. Defaults to :scope. Used with listen-for. Not compatible with listen-for-lifecycle. The selector MUST resolve when host-ref is set — it will not wait for a match to appear. hostRef listenable-element
string <CSS Selector> | "window" | "document" | "html" | "body" | "head" null
option is-debounced
With delay-ms, coalesce bursts into one trailing call (debounce). isDebounced listenable-element
boolean false
option keycode-filter
Space-separated key filters (OR). Join modifiers with + (AND, any order): shift+k tab → Shift+K or Tab. Modifiers: shift, alt, ctrl/control, meta/cmd. Name the space bar space / spacebar and the plus key plus (shift+space). Case-insensitive. keycodeFilter listenable-element
tokenlist <key | mod+key>… null
option listen-for
Space-separated event names to listen for. Defaults to click when unset (and no lifecycle list is set). listenFor listenable-element
tokenlist <EventName>… null
option listen-for-lifecycle
Space-separated element lifecycles to handle. listenForLifecycle listenable-element
tokenlist "connected" | "disconnected" | "adopted" null
option listen-once
Handle each distinct event name / lifecycle at most once. listenOnce listenable-element
boolean false
option mutate-target
@deprecated Write attributes on the target instead of firing events (sources: attr-* on this element plus form-ref fields). Use a Quark @on <event> { … } block instead — it writes State from the event without one element mutating another. Kept for compatibility; will be removed in a future major. mutateTarget
boolean false
option not-bubbles
Outgoing events use bubbles: false (default: bubble). notBubbles
boolean false
option not-cancelable
Outgoing events use cancelable: false (default: cancelable). notCancelable
boolean false
option not-composed
Outgoing events use composed: false (default: composed / cross shadow roots). notComposed
boolean false
option pathname-filter
Only handle when location.pathname is one of these values — route-aware behaviors without a separate router element. pathnameFilter listenable-element
tokenlist <pathname>… null
option prevent-default
Call preventDefault() on matched events (ignored for lifecycles). preventDefault listenable-element
boolean false
option selector-filter
Only handle events whose event.target matches this CSS selector. Does not support :scope in the selector. selectorFilter listenable-element
string <CSS Selector> null
option stop-immediate-propagation
Call stopImmediatePropagation() on matched events (ignored for lifecycles). stopImmediatePropagation listenable-element
boolean false
option stop-propagation
Call stopPropagation() on matched events (ignored for lifecycles). stopPropagation listenable-element
boolean false
option target-ref
Where outgoing events / mutations / commands apply. Unset = this element. Supports :scope for relative targeting (e.g. :scope ~ dialog). targetRef
string <CSS Selector> null
option vibrate-ms
Vibrate on handle (navigator.vibrate). Empty / 0 uses a 20ms pulse. vibrateMs listenable-element
number "20 (when attribute is present with no value)"
Provision (0) This provision property lives on the DOM node. Read / watch it with Quark’s prop("provision"), or listen for the neutron-provision event from app JS. Property Type
Events (0)
Dispatches — events that event-handler fires and their default actions. Dispatch Default Action
A "default action" is subsequent logic executed by the element if e.preventDefault() is not synchronously called on the event.
Listeners — events that event-handler listens for which will trigger subsequent actions. Listener Type Action
Commands — verbs event-handler accepts as a command event (HTML Command API): from a <button command commandfor>, an <event-handler command-name>, or a CommandEvent. Command Action
Recognized Elements (0) Child or descendant elements are recognized by event-handler and relevant to its functionality. Element Relationship Required
Styles (2)
Classes — optional classes that change the appearance of event-handler. Class Description .unstyled Skip the pointer cursor from the listenable mixin.
Variables — public CSS variables for theming event-handler. Opt-out:
If you wish to opt-out of these styles on a case-to-case basis, use property all: revert-layer. Valence.css ships this as .unstyled and .unstyled-all for the entire tree.
Variable Syntax Default
Aliases — @custom-selector synonyms so custom elements can share semantic styles. Element aliases match the host tag / .tag-* class; state aliases match attributes (nested under the element alias). Alias Kind Matches :--event-handler element event-handler, .tag-event-handler
Release notes (1)

0.1.1

  • Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
View Source

Examples

Fire a custom event

Click dispatches cart-add event with { sku: "sku-1" } in event.detail.

Open a dialog

command-name invokes the HTML Command API — here the built-in show-modal opens the sibling <dialog>. Custom commands (command-name="--close") reach any element that handles them, such as <content-drawer>, through a relative target-ref.

Close on Enter

keycode-filter gates keyboard handling — here Enter, focused in the input, invokes the drawer's --close command (local / bubbling events only).

Escape to dismiss (global)

host-ref="window" listens for keydown on the window — Escape closes the dialog even when focus is outside it.

prevent-default cancels the native action — here the wrapped <a> never navigates. Use stop-propagation / stop-immediate-propagation the same way when you need to stop bubbling.

Retarget

target-ref aims the outgoing event at another element — useful when the target sits outside of the bubble path.

Debounced input / form data

Inherited delay-ms + is-debounced coalesce noisy input into one search-query event. Demo below is debounced every 200ms.

Also demonstrated is form conversion into JSON: text -> string, checkbox -> boolean, etc. Including the data structure: detail.strict -> {detail: {strict}}.

Beta. The Nucleus Stack is in beta for a few weeks until features are stabilized and optimized.

Thanks — we'll email you when it ships.

Something went wrong. Please try again.