# loadable-element

Loading / success / error state, a `provision`, and the three events — once, for every element that does async work.

## Features

- **Three states** `is-loading` / `is-success` / `is-error`, mutually exclusive
- **Stale-while-revalidate** `did-load` is set on the first success, kept through a refresh, cleared on an error — keep content on screen during a refresh
- **One payload** The result or the error lands on `provision`
- **Three events** `{tag}-loading` / `{tag}-success` / `{tag}-error`, tag-prefixed automatically
- **Four effects** `_setLoading`, `_setSuccess`, `_setError`, `_resetLoadState` — the element decides *when*, the base does the bookkeeping

## Installation


`@excom/loadable-element` v0.2.2

```bash
pnpm add @excom/loadable-element
```

```bash
npm install @excom/loadable-element
```

```bash
yarn add @excom/loadable-element
```

### Import

```ts
import { /* … */ } from "@excom/loadable-element";
```



## Usage

Compose `LoadableElement` and call its effects from your own lifecycles. `FetchableElement` (and every element built on it) and `quark-sheet` compose it this way.

```ts
import { LoadableElement } from "@excom/loadable-element";
import { Neutron } from "@excom/neutron";

export const LoadJson = Neutron.compose([
  LoadableElement,
  Neutron({ tag: "load-json", props: { srcUrl: String, _promise: Promise } }),
])
  .onPropSet("srcUrl", ({ srcUrl }) => [
    { _setLoading: [] },
    { _promise: fetch(srcUrl).then((r) => r.json()) },
  ])
  .onPromiseResolved("_promise", (_, { _promise }) => ({ _setSuccess: [_promise] }))
  .onPromiseRejected("_promise", (_, { _promise }) => ({ _setError: [_promise] }));

LoadJson.define();
```

```html
<load-json src-url="/api/user"></load-json>
```

```quark
load-json[is-success] { $user: prop("provision"); }
load-json[is-error] [bind-message] { content: prop("provision").message; }
```

Cancelled or superseded work calls `_resetLoadState` (no event) — pair with [abortable-element](/packages/abortable-element) to abort the promise itself.

### API Reference


#### Attributes

| Name | Surface | Type | Default | Values | Description |
| --- | --- | --- | --- | --- | --- |
| `is-loading` | state | `boolean` |  |  | Work is in flight. |
| `is-success` | state | `boolean` |  |  | The most recent work finished successfully. Mutually exclusive with `is-loading` and `is-error`. |
| `is-error` | state | `boolean` |  |  | The most recent work failed. Mutually exclusive with `is-loading` and `is-success`. |
| `did-load` | state | `boolean` |  |  | 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. |

#### Provision

| Name | Type | Description |
| --- | --- | --- |
| `provision` | `unknown` | 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. |

#### Fires

| Name | Type | Description |
| --- | --- | --- |
| `{tag}-loading` | `LoadableLoadingEvent` (`CustomEvent & { type: "{tag}-loading"; detail: void; bubbles: true; cancelable: true; composed: true }`) | After `is-loading` is set (work started). |
| `{tag}-success` | `LoadableSuccessEvent` | After `is-success` is set. `event.detail` is the new `provision`. |
| `{tag}-error` | `LoadableErrorEvent` | After `is-error` is set. `event.detail` is the error payload (also stored as `provision`). |

## Release notes

### 0.2.0 (2026-09-30)

- Add `did-load`: set on the first success, kept while a refresh loads and after a cancel, and cleared on an error, so content can stay on screen during a refresh with `[did-load]` instead of `[is-success]`

### 0.1.1 (2026-09-23)

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