@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; }

fetchable-element

Composition base that owns the fetch() lifecycle for Neutron elements — build the request from attributes, a <form>, or a custom override, then track loading / success / error state automatically.

Features

  • Shared lifecycle States, provision and events come from loadable-element

  • Attribute-driven requests URL, method, headers, redirect, and credentials all configurable declaratively

  • Form-aware Point form-ref at a <form> to source action, method, enctype, and field values

  • Merged payloads Attributes, form, and custom args deep-merge (lowest → highest priority)

  • Lifecycle state is-loading / is-success / is-error managed for you

  • Provision provision is the parsed response (or error payload) for Quark prop("provision") — not a reflected attribute

  • Cancel-safe Superseded or disconnected requests are aborted via AbortableElement

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/fetchable-element@0.3.0/dist/index.umd.min.js"></script>
npm install @excom/fetchable-element
HTML Imports JS / CSS Imports
import { /* … */ } from "@excom/fetchable-element";
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
./index
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./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.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

Compose FetchableElement, then call doFetch([url, requestInit]) — usually built via getFetchArgs(customFetchArgs?) — whenever the subclass decides a request should run. Concrete consumers include <provider-fetch> (fetch on attribute change), <super-form> (fetch on submit), and <web-authn> (WebAuthn ceremonies that still round-trip to a server).

import { Neutron } from "@excom/neutron";
import { FetchableElement } from "@excom/fetchable-element";
​
export const RefreshOnClick = Neutron.compose([
  FetchableElement,
  Neutron({ tag: "refresh-on-click" }),
])
  .onEvent("click", ({ getFetchArgs }) => ({
    doFetch: [getFetchArgs()],
  }));
​
RefreshOnClick.define();
<refresh-on-click api-url="/api/status"></refresh-on-click>

Every prop, state field, and event documented below is inherited verbatim by any element that composes FetchableElement — it flattens directly into that element's own generated docs, so <provider-fetch> and friends don't redeclare it.

API Reference

Attributes (16) Role — option is configurable, state is managed by the element (read-only), or hybrid which is both. Role Attribute Type Values Prop Default option api-method
HTTP method. Always uppercased before the request is sent. apiMethod
string "GET"
option api-url
Endpoint URL. When the request has no body, the JSON payload (from form-ref or custom doFetch() args) is merged in as query params instead. apiUrl
string ""
option fetch-credentials
RequestInit.credentials mode. fetchCredentials
string "omit" | "same-origin" | "include" "include"
option fetch-redirect
RequestInit.redirect mode. Unset defers to the browser default (follow). fetchRedirect
string "follow" | "error" | "manual" null
option form-ref
CSS selector for a <form> to source the request from — its action (URL), method, enctype (Content-Type), and field values (as the JSON payload) all take priority over the matching attributes below. Omit to build the request entirely from attributes / custom doFetch() args. formRef
string <CSS Selector> null
option has-body
Force a request body even for methods that don't imply one (GET / HEAD). Already implied for POST / PUT / PATCH. hasBody
boolean false
option header-accept
Accept request header. headerAccept
string "application/json"
option header-cache-control
Cache-Control request header. Unset by default (browser default caching applies). headerCacheControl
string null
option header-content-type
Content-Type request header. Dropped entirely when the request has no body. headerContentType
string "application/json"
state did-load
Work has succeeded and no failure followed. Stays set while a refresh loads, so content can stay on screen (stale-while-revalidate): gate it on [did-load] rather than [is-success]. Cleared on error. didLoad loadable-element
boolean false
state is-error
The most recent request failed (status 400 or above, network error, or a thrown error other than AbortError). Fires with the error event. isError
boolean false
state is-error
The most recent work failed. Mutually exclusive with is-loading and is-success. isError loadable-element
boolean false
state is-loading
A request is currently in flight. isLoading
boolean false
state is-loading
Work is in flight. isLoading loadable-element
boolean false
state is-success
The most recent request resolved successfully. Mutually exclusive with is-loading and is-error. isSuccess
boolean false
state is-success
The most recent work finished successfully. Mutually exclusive with is-loading and is-error. isSuccess loadable-element
boolean false
Provision (2) 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 provision
Response payload on success, or error payload on failure. Success shape: { status, statusText, ok, headers, url, redirected, bodyUsed, type, body }. Failure shape is either that same response shape (server responded with an error status) or { message, stack } (request never completed). Not reflected as an attribute. provision
FetchResponse
provision
The result of the most recent work on success, or the error payload on failure. Shape is defined by the composing element. Not reflected as an attribute. provision loadable-element
unknown
Events (6)
Dispatches — events that 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.
Name {tag}-error
Dispatched when the request fails — status 400 or above, network error, or a thrown error. event.detail is the error payload (see provision). Not dispatched for aborted requests.

Type FetchableErrorEvent
Name {tag}-error
After is-error is set. event.detail is the error payload (also stored as provision). loadable-element

Type LoadableErrorEvent
Name {tag}-loading
Dispatched immediately before the request is sent.

Type FetchableLoadingEvent
Name {tag}-loading
After is-loading is set (work started). loadable-element

Type LoadableLoadingEvent
Name {tag}-success
Dispatched when the request resolves successfully. event.detail is the parsed response (see provision).

Type FetchableSuccessEvent
Name {tag}-success
After is-success is set. event.detail is the new provision. loadable-element

Type LoadableSuccessEvent
Listeners — events that listens for which will trigger subsequent actions. Listener Type Action
Commands — verbs 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 and relevant to its functionality. Element Relationship Required
Styles (0)
Classes — optional classes that change the appearance of . Class Description
Variables — public CSS variables for theming . 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
Release notes (4)

0.3.0

  • Add hydration of prerendered pages: an identical GET or HEAD request is answered from the page's hydration island, as often as the prerender made it, so the element announces loading then success without a network request

0.2.0

  • Add did-load from loadable-element: set on the first success, kept while a refresh runs, cleared on an error
  • Update failed-request logging to one line: an error status (400 or above) logs a warning, <tag>: request failed with the response, which the default KitLogger level hides; a network or parse failure logs an error; an abort logs nothing

0.1.2

  • Fix typo in error message.

0.1.1

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

There are no demos for this package — see provider-fetch for FetchableElement in action against a real endpoint.

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.