@excom/neutron 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/neutron.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/neutron.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +9 -0
- package/package.json +45 -0
- package/rush-logs/neutron.apply-exports.cache.log +1 -0
- package/rush-logs/neutron.apply-exports.log +1 -0
- package/rush-logs/neutron.build_package-metas.cache.log +1 -0
- package/rush-logs/neutron.build_package-metas.log +1 -0
- package/src/command.ts +102 -0
- package/src/common-element.ts +377 -0
- package/src/constants.ts +101 -0
- package/src/devtools-hook.ts +93 -0
- package/src/lifecycle-configs.ts +304 -0
- package/src/neutron-element.ts +72 -0
- package/src/neutron-error.ts +6 -0
- package/src/neutron-internal.ts +550 -0
- package/src/neutron.ts +36 -0
- package/src/types/effect.types.ts +104 -0
- package/src/types/element.types.ts +263 -0
- package/src/types/index.ts +4 -0
- package/src/types/new.types.ts +159 -0
- package/src/types/shared.types.ts +25 -0
- package/src/utils/effect.ts +357 -0
- package/src/utils/element.ts +382 -0
- package/src/utils/index.ts +2 -0
- package/support/docs/COMMANDS.md +58 -0
- package/support/docs/COMPOSE.md +32 -0
- package/support/docs/DEBUG.md +11 -0
- package/support/docs/DEFINE.md +20 -0
- package/support/docs/EFFECTS.md +49 -0
- package/support/docs/EVENTS.md +57 -0
- package/support/docs/LIFECYCLES.md +69 -0
- package/support/docs/METHODS.md +49 -0
- package/support/docs/PROMISE_PROPS.md +29 -0
- package/support/docs/PROPS.md +64 -0
- package/support/docs/PROP_REACTIONS.md +35 -0
- package/support/docs/PROVISION.md +31 -0
- package/support/docs/README.md +118 -0
- package/support/docs/RECOMPOSE.md +70 -0
- package/support/docs/TYPESCRIPT.md +51 -0
- package/support/docs-sections.json +42 -0
- package/support/package-meta.json +129 -0
- package/support/tests/commands.test.ts +330 -0
- package/support/tests/common-element.test.ts +342 -0
- package/support/tests/devtools-hook.test.ts +209 -0
- package/support/tests/devtools-renderer.test.ts +125 -0
- package/support/tests/effects.test.ts +253 -0
- package/support/tests/element-config.test.ts +331 -0
- package/support/tests/entry.test.ts +68 -0
- package/support/tests/lifecycles.test.ts +489 -0
- package/support/tests/loop-guard.test.ts +162 -0
- package/support/tests/neutron.test.ts +1286 -0
- package/support/tests/recompose.test.ts +129 -0
- package/support/tests/utils.test.ts +75 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Lifecycles
|
|
2
|
+
|
|
3
|
+
Chain `.on*` handlers that return effects; Neutron applies them, tracks listeners, and routes errors.
|
|
4
|
+
|
|
5
|
+
## Handlers
|
|
6
|
+
|
|
7
|
+
Each handler receives the element first, then any lifecycle-specific argument, and returns an [effect](./EFFECTS.md). Prefer effects over mutating the element directly. Every `on*` has an `off*` twin that unregisters the same function.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
Neutron({ tag: "panel-host", props: { isReady: Boolean } })
|
|
11
|
+
.onConstructed(() => ({ /* runs in the constructor, before connect */ }))
|
|
12
|
+
.onConnected(() => ({
|
|
13
|
+
// every connect, including reconnects
|
|
14
|
+
isReady: true,
|
|
15
|
+
emit: ["panel-host-ready"],
|
|
16
|
+
}))
|
|
17
|
+
.onDisconnected((el) => ({
|
|
18
|
+
// tear down; `el.isMoving` is true when a disconnect is followed by a connect in the same tick
|
|
19
|
+
isReady: false,
|
|
20
|
+
}))
|
|
21
|
+
.onAdopted(() => ({}))
|
|
22
|
+
.onError((_el, err) => {
|
|
23
|
+
console.error(err);
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Disconnect is settled one microtask after `disconnectedCallback`. If the element is re-inserted before then (a DOM move), `onDisconnected` and `onConnected` both still run, with `isMoving` set. An error thrown by any handler is routed to `onError(el, error)`; without an `onError`, it is rethrown.
|
|
28
|
+
|
|
29
|
+
Reactions to prop changes, events, commands and promises are their own pages: [Prop reactions](./PROP_REACTIONS.md), [Events](./EVENTS.md), [Commands](./COMMANDS.md), [Promise props](./PROMISE_PROPS.md).
|
|
30
|
+
|
|
31
|
+
## Destructure the element argument
|
|
32
|
+
|
|
33
|
+
Prefer `({ prop }) => …` over `(el) => …`. A handler that only ever sees the values it names cannot reach for `el.setAttribute`, `el.querySelector(…).value = …` or any other imperative mutation — reading the signature is enough to know the handler is pure, and the effect it returns is the whole story.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
Neutron({ tag: "price-tag", props: { amount: Number, currency: String } })
|
|
37
|
+
.onPropChanged(["amount", "currency"], ({ amount, currency }) => ({
|
|
38
|
+
ariaLabel: `${amount} ${currency}`,
|
|
39
|
+
}));
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Pitfall: stale values in async callbacks
|
|
43
|
+
|
|
44
|
+
Destructuring copies the values at call time. If the handler starts asynchronous work and the callback reads those copies, it sees the element as it was when the work started, not when it finished:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// ✗ `amount` here is whatever it was when the fetch began
|
|
48
|
+
.onConnected(({ apiUrl, amount }) => ({
|
|
49
|
+
addListener: ["price-tag-refresh", () => fetch(apiUrl).then(() => console.log(amount))],
|
|
50
|
+
}))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Make the callback a `defineMethods` method instead. [Methods](./METHODS.md) are effectors too — Neutron calls them with the live element first — so the callback destructures fresh values when it actually runs, and its return value is applied as an effect. The lifecycle keeps `el` only to reach the method (methods are bound to the element; call them as `el.method(…)`):
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
Neutron({ tag: "price-tag", props: { apiUrl: String, amount: Number } })
|
|
57
|
+
.defineMethods({
|
|
58
|
+
// runs later, with the element as it is *then*
|
|
59
|
+
applyQuote: ({ amount }, quote: { rate: number }) => ({
|
|
60
|
+
amount: amount * quote.rate,
|
|
61
|
+
}),
|
|
62
|
+
})
|
|
63
|
+
.onConnected((el) => ({
|
|
64
|
+
addListener: [
|
|
65
|
+
"price-tag-refresh",
|
|
66
|
+
() => fetch(el.apiUrl).then((r) => r.json()).then((quote) => el.applyQuote(quote)),
|
|
67
|
+
],
|
|
68
|
+
}));
|
|
69
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Methods
|
|
2
|
+
|
|
3
|
+
Methods are effectors like lifecycles: they receive the live element, return an effect, and can hand a value back to the caller.
|
|
4
|
+
|
|
5
|
+
## Defining methods
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
Neutron({ tag: "tally-counter", props: { tallyCount: Number } })
|
|
9
|
+
.defineMethods({
|
|
10
|
+
increment: ({ tallyCount }, step = 1) => ({
|
|
11
|
+
tallyCount: (tallyCount ?? 0) + step,
|
|
12
|
+
returns: (tallyCount ?? 0) + step, // value returned to the caller
|
|
13
|
+
}),
|
|
14
|
+
})
|
|
15
|
+
.define();
|
|
16
|
+
|
|
17
|
+
// el.increment(2) → number
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Element first, then the call arguments. Other effects can call them by name: `increment: [2]`. Because Neutron calls a method with the element as it is *then*, methods are also the right shape for async callbacks — see [Lifecycles](./LIFECYCLES.md#md-pitfall-stale-values-in-async-callbacks).
|
|
21
|
+
|
|
22
|
+
### Pre-declaring signatures
|
|
23
|
+
|
|
24
|
+
`defineMethods` types `element` as the element *before* that call, so a method cannot reach a sibling defined in the same object. `withTypes<T>()` is a type-only step (no runtime effect) that puts the signatures on the element type first — methods are bound and the element argument is stripped, so declare them as the element sees them. It also makes `typeof Builder.CustomElement` usable for module-level helpers.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
interface Methods {
|
|
28
|
+
flush: FrameRequestCallback;
|
|
29
|
+
schedule: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Ticker = Neutron({ tag: "tick-er", props: { tickCount: Number } })
|
|
33
|
+
.withTypes<Methods>();
|
|
34
|
+
|
|
35
|
+
type El = typeof Ticker.CustomElement;
|
|
36
|
+
|
|
37
|
+
const label = (el: El) => `${el.tickCount}`;
|
|
38
|
+
|
|
39
|
+
Ticker.defineMethods({
|
|
40
|
+
flush: ({ tickCount }) => ({ tickCount: tickCount + 1 }),
|
|
41
|
+
schedule: ({ flush }) => {
|
|
42
|
+
requestAnimationFrame(flush); // sibling method, no cast
|
|
43
|
+
},
|
|
44
|
+
}).define();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Keep the imperative surface small
|
|
48
|
+
|
|
49
|
+
If a method is something the consuming app may want to invoke, accept it as a command (`onCommand("--verb")`, see [Commands](./COMMANDS.md)); if it is something the app may want to cancel, fire an event with a default action (`onEventDefault`, see [Events](./EVENTS.md)). Neither needs a public method.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Promise props
|
|
2
|
+
|
|
3
|
+
Store a promise in a prop and react to its settlement; replacing it cancels the stale one.
|
|
4
|
+
|
|
5
|
+
## Resolve / reject handlers
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
Neutron({
|
|
9
|
+
tag: "lazy-item",
|
|
10
|
+
props: {
|
|
11
|
+
srcPromise: Promise,
|
|
12
|
+
provision: Object,
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
.onConnected(() => ({
|
|
16
|
+
srcPromise: fetch("/api/item").then((r) => r.json()),
|
|
17
|
+
}))
|
|
18
|
+
.onPromiseResolved("srcPromise", (_el, result) => ({
|
|
19
|
+
srcPromise: null,
|
|
20
|
+
provision: result.srcPromise,
|
|
21
|
+
}))
|
|
22
|
+
.onPromiseRejected("srcPromise", () => ({ srcPromise: null }));
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`result` maps the prop name to the resolved value (or the rejection reason).
|
|
26
|
+
|
|
27
|
+
## Cancellation
|
|
28
|
+
|
|
29
|
+
Assigning a new promise or `null` while the previous one is pending cancels it — the stale settlement never reaches the handlers.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Props
|
|
2
|
+
|
|
3
|
+
Declare an element's state once; primitives become dashed attributes that CSS and Quark select on, rich values stay on the instance.
|
|
4
|
+
|
|
5
|
+
## Declaring props
|
|
6
|
+
|
|
7
|
+
Shorthand constructors reflect primitives to dashed attributes. Rich config adds defaults, validation, storage, and custom serialize / deserialize.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Neutron, TokenList } from "@excom/neutron";
|
|
11
|
+
|
|
12
|
+
Neutron({
|
|
13
|
+
tag: "usage-meter",
|
|
14
|
+
props: {
|
|
15
|
+
unitLabel: String, // reflects ↔ `unit-label`
|
|
16
|
+
maxCount: Number, // reflects ↔ `max-count`
|
|
17
|
+
isOpen: Boolean, // presence attribute `is-open`
|
|
18
|
+
featureTags: TokenList, // space-separated tokens ↔ `feature-tags`, read as `string[]`
|
|
19
|
+
// non-reflecting by default:
|
|
20
|
+
payload: Object,
|
|
21
|
+
items: Array,
|
|
22
|
+
srcPromise: Promise,
|
|
23
|
+
// rich config:
|
|
24
|
+
maxValue: {
|
|
25
|
+
type: Number,
|
|
26
|
+
defaultValue: () => 100,
|
|
27
|
+
isValid: (n) => n > 0,
|
|
28
|
+
},
|
|
29
|
+
// element references are always weak — never pin another node:
|
|
30
|
+
inputEl: { type: HTMLInputElement, store: "weak" },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Rich config keys
|
|
36
|
+
|
|
37
|
+
| Key | Purpose |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `type` | Constructor. `String` / `Number` / `Boolean` / `TokenList` reflect to an attribute; everything else is instance-only. |
|
|
40
|
+
| `defaultValue` | `() => value`, returned when the prop is nullish or invalid. |
|
|
41
|
+
| `isValid` | `(value) => boolean`. Invalid values fall back to the default; for `TokenList` the invalid tokens are filtered out instead. |
|
|
42
|
+
| `attr` | Override the attribute name, or `false` to keep a primitive off the attribute. |
|
|
43
|
+
| `store` | `"weak"` holds the value in a `WeakRef` and derefs on read. Required for every prop that references another element, so a removed node can be collected. |
|
|
44
|
+
| `serialize` / `deserialize` | Transform on write / read. |
|
|
45
|
+
|
|
46
|
+
## TokenList
|
|
47
|
+
|
|
48
|
+
`TokenList` is exported by this package. It marks a space-separated attribute (`feature-tags="a b"`) whose property value is a plain `string[]` — the element-side equivalent of `class`. Prefer it over `Array` whenever the list belongs in the document, so CSS and Quark can select on it (`usage-meter[feature-tags~="a"]`).
|
|
49
|
+
|
|
50
|
+
## Built-in instance props
|
|
51
|
+
|
|
52
|
+
`isMounted`, `isMoving`, `isAdopted`, `wasMounted` exist on every element. They are instance-only; list them in `reflectDefaultProps: ["isMounted"]` to reflect them as attributes (`is-mounted`).
|
|
53
|
+
|
|
54
|
+
## Naming rules
|
|
55
|
+
|
|
56
|
+
Enforced at definition time or by convention:
|
|
57
|
+
|
|
58
|
+
- Custom attributes must contain a dash (`max-count`, `is-open`), so they can never collide with a native attribute — now or in the future. Dev mode warns on dash-less, `data-*`, and `aria-*` attributes.
|
|
59
|
+
- Booleans read as assertions: `is-loading`, `did-fail`, `has-rendered`, `should-fetch`.
|
|
60
|
+
- Events are tag-prefixed (`press-tracker-press`), never bare (`change`).
|
|
61
|
+
- Attribute names may not start with `q-`, `n-`, `on-`, or `off-` (reserved by Quark and Neutron).
|
|
62
|
+
- Prop names may not shadow Neutron internals or effect keywords (`returns`, `content`, lifecycle names, `_n_`, `_q_`).
|
|
63
|
+
- Private state and methods take a leading underscore.
|
|
64
|
+
- Loosely couple: element-typed props use `store: "weak"`, and anything else that holds a node is cleared in `onDisconnected`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Prop reactions
|
|
2
|
+
|
|
3
|
+
React to one prop, or a batch of them, with the previous values in hand.
|
|
4
|
+
|
|
5
|
+
## Handlers
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
Neutron({ tag: "echo-field", props: { fieldValue: String, isFilled: Boolean } })
|
|
9
|
+
.onPropSet("fieldValue", () => ({
|
|
10
|
+
/* fieldValue became truthy for its type */
|
|
11
|
+
isFilled: true,
|
|
12
|
+
}))
|
|
13
|
+
.onPropUnset("fieldValue", () => ({
|
|
14
|
+
/* fieldValue became falsy / removed */
|
|
15
|
+
isFilled: false,
|
|
16
|
+
}))
|
|
17
|
+
.onPropChanged("fieldValue", ({ fieldValue }, previous) => ({
|
|
18
|
+
/* any change, including unset → set; `previous.fieldValue` is the old value */
|
|
19
|
+
emit: ["echo-field-change", { detail: { fieldValue, previous: previous.fieldValue } }],
|
|
20
|
+
}))
|
|
21
|
+
// one handler for a batch of props — runs when any of them changed
|
|
22
|
+
.onEffect(["fieldValue", "isFilled"], (el, previous) => ({
|
|
23
|
+
emit: ["echo-field-effect", { detail: { previous } }],
|
|
24
|
+
}));
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`onPropChanged` and `onEffect` take one name or an array of names. The second argument maps every prop that changed in the batch to its **previous** value.
|
|
28
|
+
|
|
29
|
+
## Mount gating
|
|
30
|
+
|
|
31
|
+
Reactions run only on mounted elements. Changes made before the first connect (attributes parsed from HTML, props set on a detached element) are kept and flushed as one batch on first mount, so reaction handlers always see the settled initial state.
|
|
32
|
+
|
|
33
|
+
## No self-writes
|
|
34
|
+
|
|
35
|
+
A handler must not set the same prop it is reacting to — Neutron throws a `NeutronError`. Child effects on element-typed props are the exception, so `.onPropSet("inputEl", () => ({ inputEl: { addListener: [...] } }))` is allowed.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Provision
|
|
2
|
+
|
|
3
|
+
One property carries everything rich an element publishes to the document, so Quark and app JS have a single place to read it.
|
|
4
|
+
|
|
5
|
+
## Declaring a provision
|
|
6
|
+
|
|
7
|
+
All public, rich data an element exposes to the document goes through one property: `provision`. Declare it as `provision: Object` (or a typed constructor), set it from an effect, and tag it `@provision` in JSDoc.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
Neutron({
|
|
11
|
+
tag: "provider-ping",
|
|
12
|
+
props: { provision: Object },
|
|
13
|
+
})
|
|
14
|
+
.onConnected(() => ({ provision: { at: Date.now() } }))
|
|
15
|
+
.define();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Reading it
|
|
19
|
+
|
|
20
|
+
Every set emits `neutron-provision` for app JS. Quark reads it on the element itself with `prop("provision")` and re-runs when it is assigned — assign a new object rather than mutating the old one.
|
|
21
|
+
|
|
22
|
+
```quark
|
|
23
|
+
provider-ping {
|
|
24
|
+
$ping: prop("provision");
|
|
25
|
+
[bind-at] { content: $ping.at; }
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Ordering
|
|
30
|
+
|
|
31
|
+
Within one [effect](./EFFECTS.md), `provision` is always applied last, so listeners of `neutron-provision` see every other prop already settled.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# neutron
|
|
2
|
+
|
|
3
|
+
Define typed custom elements with effect-based lifecycles — props, events, and compose without rewriting the Custom Elements boilerplate.
|
|
4
|
+
|
|
5
|
+
Neutron is the element factory of the Nucleus Stack: `Neutron({ tag, props })` returns a builder you chain lifecycles onto, then `define()`. Every Nucleus Kit element is a Neutron element, and so is every element you write yourself.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Declarative factory** `Neutron({ tag, props }).… .define()`
|
|
10
|
+
- **Typed props** Primitives and `TokenList` reflect to dashed attributes; objects / arrays / elements / promises stay on the instance
|
|
11
|
+
- **Effect returns** Lifecycles / methods return a POJO (or an array of them) that sets props, emits, listens, calls methods, and styles
|
|
12
|
+
- **Fine-grained reactions** `onPropSet` / `Unset` / `Changed` / `onEffect`
|
|
13
|
+
- **Events & broadcasts** Tag-prefixed custom events, cancelable default actions, channel broadcasts
|
|
14
|
+
- **Commands** `onCommand("--verb")` handles the HTML Command API — `<button command commandfor>` needs no custom element
|
|
15
|
+
- **Listener cleanup** Listeners added through effects are removed on disconnect and restored on reconnect
|
|
16
|
+
- **Compose** Combine builders (`Neutron.compose`) for mixin-style packages
|
|
17
|
+
- **Recompose** Import a package's raw builder, add / remove lifecycles and methods, then `define()` it yourself
|
|
18
|
+
- **DevTools** `Neutron.attachDevtools()` hooks the Nucleus DevTools extension
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
<include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
An element owns its own state (attributes) and announces changes (events). It never renders children or reaches into siblings — coordination belongs to Quark. The rules these examples follow are collected in [Best Practices](/nucleus/docs/best_practices) and [Creating Elements](/nucleus/docs/creating_elements).
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Neutron } from "@excom/neutron";
|
|
30
|
+
|
|
31
|
+
export const PressTracker = Neutron({
|
|
32
|
+
tag: "press-tracker",
|
|
33
|
+
props: {
|
|
34
|
+
pressCount: { type: Number, defaultValue: () => 0 }, // reflects ↔ `press-count`
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
.onEvent("click", ({ pressCount }) => ({
|
|
38
|
+
// effects are declarative instructions, not imperative mutations
|
|
39
|
+
pressCount: pressCount + 1,
|
|
40
|
+
emit: ["press-tracker-press", { detail: { pressCount: pressCount + 1 } }],
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
PressTracker.define();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<press-tracker press-count="0">
|
|
48
|
+
<button>Press</button>
|
|
49
|
+
</press-tracker>
|
|
50
|
+
<!-- `press-tracker[press-count="3"]` is now a CSS / Quark selector -->
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Documentation
|
|
54
|
+
|
|
55
|
+
Defining elements
|
|
56
|
+
|
|
57
|
+
- [Props](./PROPS.md) — typed props, reflection, `TokenList`, naming rules
|
|
58
|
+
- [Provision](./PROVISION.md) — the one property for published rich data
|
|
59
|
+
- [TypeScript](./TYPESCRIPT.md) — global element types, `ConstructorType`
|
|
60
|
+
|
|
61
|
+
Behavior
|
|
62
|
+
|
|
63
|
+
- [Lifecycles](./LIFECYCLES.md) — `onConnected` & co., destructuring, async pitfalls
|
|
64
|
+
- [Effects](./EFFECTS.md) — the object a handler returns
|
|
65
|
+
- [Prop reactions](./PROP_REACTIONS.md) — `onPropSet` / `Unset` / `Changed` / `onEffect`
|
|
66
|
+
- [Methods](./METHODS.md) — methods as effectors
|
|
67
|
+
- [Events](./EVENTS.md) — emits, default actions, broadcasts, listener cleanup
|
|
68
|
+
- [Commands](./COMMANDS.md) — `onCommand` for `--verb` commands, the `command` effect
|
|
69
|
+
- [Promise props](./PROMISE_PROPS.md) — `onPromiseResolved` / `Rejected`
|
|
70
|
+
|
|
71
|
+
Composition
|
|
72
|
+
|
|
73
|
+
- [Compose](./COMPOSE.md) — stack builders into mixin-style packages
|
|
74
|
+
- [Recompose](./RECOMPOSE.md) — edit a packaged element before defining it
|
|
75
|
+
|
|
76
|
+
Runtime
|
|
77
|
+
|
|
78
|
+
- [Define](./DEFINE.md) — `define()` and class introspection
|
|
79
|
+
- [Debug](./DEBUG.md) — DevTools hook, loop guard
|
|
80
|
+
|
|
81
|
+
### Examples
|
|
82
|
+
|
|
83
|
+
#### State on connect
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
Neutron({ tag: "ready-flag", props: { isReady: Boolean } })
|
|
87
|
+
.onConnected(() => ({ isReady: true, emit: ["ready-flag-ready"] }))
|
|
88
|
+
.define();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Child element effect across handlers
|
|
92
|
+
|
|
93
|
+
Assign an element prop, then react to it with a nested effect. Listener callbacks that return effects must be `defineMethods` methods:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
Neutron({
|
|
97
|
+
tag: "focus-host",
|
|
98
|
+
props: {
|
|
99
|
+
inputEl: { type: HTMLInputElement, store: "weak" },
|
|
100
|
+
isFocused: Boolean,
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
.defineMethods({
|
|
104
|
+
handleFocus: () => ({ isFocused: true, emit: ["focus-host-focus"] }),
|
|
105
|
+
handleBlur: () => ({ isFocused: false }),
|
|
106
|
+
})
|
|
107
|
+
.onConnected((el) => ({
|
|
108
|
+
inputEl: el.querySelector("input"),
|
|
109
|
+
}))
|
|
110
|
+
.onPropSet("inputEl", ({ handleFocus, handleBlur }) => ({
|
|
111
|
+
inputEl: {
|
|
112
|
+
addListeners: [
|
|
113
|
+
["focus", handleFocus],
|
|
114
|
+
["blur", handleBlur],
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Recompose
|
|
2
|
+
|
|
3
|
+
A builder is open until `define()` runs: import a package's raw builder, edit it, then register it yourself.
|
|
4
|
+
|
|
5
|
+
## Import the raw builder
|
|
6
|
+
|
|
7
|
+
Every element package ships two entries: `index.ts` (calls `.define()` and registers the global types) and the raw element source (`<element>.ts`), which only exports the builder. Import the raw entry, edit it, then register it yourself:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// app.ts — never import "@excom/detect-browser" (or nucleus-kit) here: that entry defines the element immediately
|
|
11
|
+
import { DetectBrowser } from "@excom/detect-browser/detect-browser";
|
|
12
|
+
|
|
13
|
+
DetectBrowser
|
|
14
|
+
// add lifecycles / methods the package did not ship
|
|
15
|
+
.onPropSet("isStandalone", () => ({ emit: ["detect-browser-standalone"] }))
|
|
16
|
+
.defineMethods({ clearInfo: () => ({ provision: null }) })
|
|
17
|
+
.define();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Most recomposition is exactly that: chaining new lifecycles onto a packaged element. Unregistering is rarer and goes through `builtConfig`.
|
|
21
|
+
|
|
22
|
+
## builtConfig
|
|
23
|
+
|
|
24
|
+
Every builder exposes its definition as `builtConfig`, a plain object you can read before `define()`:
|
|
25
|
+
|
|
26
|
+
| Key | Shape |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `tag` | The packaged tag name (also the event prefix). |
|
|
29
|
+
| `props` | `{ [propName]: PropConfig }` — the normalized prop configs (`type`, `attr`, `defaultValue`, …). |
|
|
30
|
+
| `events` / `broadcasts` | `{ [name]: { prefixWithTag? } }` |
|
|
31
|
+
| `methods` | `[name, fn][]` in definition order. |
|
|
32
|
+
| `lifecycles` | `{ constructed, connected, adopted, disconnected, error, effect, propSet, propUnset, propChanged, promiseResolved, promiseRejected, broadcast, event, eventDefault }`, each an array of `[names, handler]` pairs in registration order. `names` is the prop / event list the handler was registered with (`[]` for lifecycles without one). |
|
|
33
|
+
| `reflectDefaultProps` / `definitionOpts` | As passed to `Neutron()`. |
|
|
34
|
+
|
|
35
|
+
## Removing a lifecycle
|
|
36
|
+
|
|
37
|
+
Removal needs the original handler reference, because `off*` matches by function identity. Index into `builtConfig.lifecycles.<lifecycle>[entry][1]` and pass that handler to the matching `off*`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// stop detecting on connect; the app calls `el.setBrowserInfo()` when it wants to
|
|
41
|
+
DetectBrowser.offConnected(DetectBrowser.builtConfig.lifecycles.connected[0][1]);
|
|
42
|
+
|
|
43
|
+
// named lifecycles: pass the names too — only those names are detached, the handler keeps any others
|
|
44
|
+
DetectBrowser.offPropSet("isStandalone", DetectBrowser.builtConfig.lifecycles.propSet[0][1]);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Indexing by position is intentional for now (a friendlier handle may come later); read `builtConfig.lifecycles.<lifecycle>` once to see which entry you are after.
|
|
48
|
+
|
|
49
|
+
## Adding props
|
|
50
|
+
|
|
51
|
+
Adding props goes through [`Neutron.compose`](./COMPOSE.md), which also leaves the imported builder untouched (it deep-clones):
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { Neutron } from "@excom/neutron";
|
|
55
|
+
|
|
56
|
+
export const StampedDetectBrowser = Neutron.compose([
|
|
57
|
+
DetectBrowser,
|
|
58
|
+
Neutron({ tag: "detect-browser", props: { detectedAt: String } }),
|
|
59
|
+
]).onPropSet("provision", () => ({ detectedAt: new Date().toISOString() }));
|
|
60
|
+
|
|
61
|
+
StampedDetectBrowser.define();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Rules
|
|
65
|
+
|
|
66
|
+
- Everything must happen before `define()`. The runtime config is built at that moment; later `on*` / `off*` / `defineMethods` calls return the builder but change nothing.
|
|
67
|
+
- The raw builder is a module singleton — an in-place edit is visible to every importer. Use `Neutron.compose` when you want a modified copy instead.
|
|
68
|
+
- Added handlers run after the packaged ones for the same lifecycle (registration order) and batch into the same effect pass.
|
|
69
|
+
- `define("other-tag")` registers the class under a different name. Events configured with `prefixWithTag` keep the packaged prefix, because the prefix comes from the builder's `tag`, not from the registered name.
|
|
70
|
+
- Skipping `index.ts` also skips its `declare global` block — add your own (see [TypeScript](./TYPESCRIPT.md)) if you want the tag typed.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# TypeScript
|
|
2
|
+
|
|
3
|
+
Expose an element's inferred type the way the DOM exposes its own elements, so queries and `createElement` need no casts.
|
|
4
|
+
|
|
5
|
+
## Global element types
|
|
6
|
+
|
|
7
|
+
`Neutron()` infers the element type from `props` and `defineMethods`. Publish it as a global `HTML*Element` interface plus an `HTMLElementTagNameMap` entry, so `document.querySelector("press-tracker")` and `document.createElement("press-tracker")` are typed without casts:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// index.ts — the package entry defines and types the element
|
|
11
|
+
import { PressTracker } from "./press-tracker";
|
|
12
|
+
|
|
13
|
+
PressTracker.define();
|
|
14
|
+
|
|
15
|
+
export { PressTracker };
|
|
16
|
+
|
|
17
|
+
type T_HTMLPressTrackerElement = typeof PressTracker.CustomElement;
|
|
18
|
+
declare global {
|
|
19
|
+
interface HTMLPressTrackerElement extends T_HTMLPressTrackerElement {}
|
|
20
|
+
interface Window {
|
|
21
|
+
HTMLPressTrackerElement: HTMLPressTrackerElement;
|
|
22
|
+
}
|
|
23
|
+
interface HTMLElementTagNameMap {
|
|
24
|
+
"press-tracker": HTMLPressTrackerElement;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export type { HTMLPressTrackerElement };
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`CustomElement` is a type-only handle on the builder (there is no runtime value); `Props` is the inferred props object.
|
|
31
|
+
|
|
32
|
+
## Typed rich props
|
|
33
|
+
|
|
34
|
+
To type a rich prop more precisely than its constructor allows, cast the constructor with `ConstructorType<T>`:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { ConstructorType, Neutron } from "@excom/neutron";
|
|
38
|
+
|
|
39
|
+
type FeatureInfo = { fullSupport: string[]; noSupport: string[] };
|
|
40
|
+
|
|
41
|
+
Neutron({
|
|
42
|
+
tag: "detect-features",
|
|
43
|
+
props: {
|
|
44
|
+
provision: Object as unknown as ConstructorType<FeatureInfo>, // el.provision: FeatureInfo
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Event types
|
|
50
|
+
|
|
51
|
+
Document Neutron-emitted events with `TEvent` plus `type` and `detail` — see [Events](./EVENTS.md#md-typing-events).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sections": [
|
|
3
|
+
{
|
|
4
|
+
"id": "defining-elements",
|
|
5
|
+
"title": "Defining Elements",
|
|
6
|
+
"docs": [
|
|
7
|
+
"props",
|
|
8
|
+
"provision",
|
|
9
|
+
"typescript"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "behavior",
|
|
14
|
+
"title": "Behavior",
|
|
15
|
+
"docs": [
|
|
16
|
+
"lifecycles",
|
|
17
|
+
"effects",
|
|
18
|
+
"prop_reactions",
|
|
19
|
+
"methods",
|
|
20
|
+
"events",
|
|
21
|
+
"commands",
|
|
22
|
+
"promise_props"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "composition",
|
|
27
|
+
"title": "Composition",
|
|
28
|
+
"docs": [
|
|
29
|
+
"compose",
|
|
30
|
+
"recompose"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "runtime",
|
|
35
|
+
"title": "Runtime",
|
|
36
|
+
"docs": [
|
|
37
|
+
"define",
|
|
38
|
+
"debug"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|