elements-kit 0.0.1 → 0.0.3
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/README.md +325 -15
- package/dist/attributes.d.mts +90 -0
- package/dist/attributes.mjs +81 -0
- package/dist/builder/dom.d.mts +212 -0
- package/dist/builder/dom.mjs +205 -0
- package/dist/builder/index.d.mts +2 -0
- package/dist/builder/index.mjs +113 -0
- package/dist/element-CCHXkEsj.mjs +262 -0
- package/dist/index-C6xwOPCO.d.mts +263 -0
- package/dist/index-DJejH8Ff.d.mts +89 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.mjs +127 -0
- package/dist/jsx-runtime/index.d.mts +70 -0
- package/dist/jsx-runtime/index.mjs +2 -0
- package/dist/lib-B2drrxlV.mjs +15 -0
- package/dist/polyfill-BEL-HWkO.d.mts +14 -0
- package/dist/signals/index.d.mts +2 -0
- package/dist/signals/index.mjs +2 -0
- package/dist/signals/lib/media.d.mts +14 -0
- package/dist/signals/lib/media.mjs +26 -0
- package/dist/signals/lib/react.d.mts +62 -0
- package/dist/signals/lib/react.mjs +82 -0
- package/dist/signals-Cr7xgAJH.mjs +966 -0
- package/dist/slot.d.mts +73 -0
- package/dist/slot.mjs +142 -0
- package/package.json +69 -15
- package/dist/builder.d.mts +0 -11799
- package/dist/builder.mjs +0 -208
- package/dist/chunk-CFhWmker.mjs +0 -36
- package/dist/core.d.mts +0 -14
- package/dist/core.mjs +0 -74
- package/dist/signals-BGUPt0zl.mjs +0 -13
- package/dist/signals-BzhB4Ch2.d.mts +0 -10
- package/dist/signals.d.mts +0 -3
- package/dist/signals.mjs +0 -5
package/README.md
CHANGED
|
@@ -1,22 +1,332 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ElementsKit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Universal reactive primitives for the web.** Signals, JSX, and custom elements that work anywhere — standalone, inside React, Vue, or any framework, or as the foundation of your own component model.
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
|
-
|
|
6
|
+
import { signal, computed, reactive } from "elements-kit/signals";
|
|
7
|
+
import { attributes, ATTRIBUTES as attr } from "elements-kit/attributes";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
@attributes
|
|
10
|
+
class CounterElement extends HTMLElement {
|
|
11
|
+
static [attr] = {
|
|
12
|
+
count(this: CounterElement, value: string | null) {
|
|
13
|
+
this.count = Number(value ?? 0);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
@reactive() count = 0;
|
|
18
|
+
doubled = computed(() => this.count * 2);
|
|
19
|
+
|
|
20
|
+
connectedCallback() {
|
|
21
|
+
this.appendChild(
|
|
22
|
+
<section>
|
|
23
|
+
<p>Count: <strong>{() => this.count}</strong> — Doubled: <strong>{this.doubled}</strong></p>
|
|
24
|
+
<button onClick={() => this.count++}>+1</button>{" "}
|
|
25
|
+
<button onClick={() => this.count--}>−1</button>
|
|
26
|
+
</section> as Element,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
customElements.define("x-counter", CounterElement);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Why ElementsKit
|
|
37
|
+
|
|
38
|
+
Modern UI frameworks solve reactivity and rendering together — you adopt the whole system or none of it. ElementsKit separates the two:
|
|
39
|
+
|
|
40
|
+
- **Signals** are the reactive core — fine-grained, framework-agnostic, composable with any rendering model.
|
|
41
|
+
- **JSX** compiles to real `document.createElement` calls — no virtual DOM, no runtime overhead.
|
|
42
|
+
- **Custom elements** are standard browser components — ElementsKit enhances them with signals and JSX without wrapping or abstracting the platform.
|
|
43
|
+
|
|
44
|
+
Use one piece, or all three. Integrate with React for complex UIs. Build web components that work anywhere HTML does.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
npm install elements-kit
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Configure JSX in your `tsconfig.json`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"compilerOptions": {
|
|
59
|
+
"jsx": "react-jsx",
|
|
60
|
+
"jsxImportSource": "elements-kit"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Signals
|
|
68
|
+
|
|
69
|
+
Fine-grained reactive state. Signals track their dependencies automatically — only the exact computeds and effects that depend on a changed signal are re-evaluated.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { signal, computed, effect, batch, untracked, onCleanup } from "elements-kit/signals";
|
|
73
|
+
|
|
74
|
+
const count = signal(0);
|
|
75
|
+
const doubled = computed(() => count() * 2);
|
|
76
|
+
|
|
77
|
+
const stop = effect(() => {
|
|
78
|
+
console.log("count:", count()); // runs on every change
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
count(1); // → count: 1
|
|
82
|
+
count(2); // → count: 2
|
|
83
|
+
stop(); // unsubscribe
|
|
84
|
+
|
|
85
|
+
batch(() => { count(10); count(20); }); // single notification
|
|
86
|
+
|
|
87
|
+
const raw = untracked(() => count()); // read without subscribing
|
|
88
|
+
|
|
89
|
+
effect(() => {
|
|
90
|
+
const id = setInterval(() => count(count() + 1), 1000);
|
|
91
|
+
onCleanup(() => clearInterval(id)); // runs before re-run or on stop
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Store
|
|
96
|
+
|
|
97
|
+
A **store** is a class whose fields are made reactive with `@reactive`. It holds shared state — no `render()`, no DOM — and any subscriber updates automatically.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { reactive, computed } from "elements-kit/signals";
|
|
101
|
+
|
|
102
|
+
export class CartStore {
|
|
103
|
+
@reactive() items: { name: string; price: number }[] = [];
|
|
104
|
+
@reactive() discount = 0;
|
|
105
|
+
|
|
106
|
+
total = computed(() =>
|
|
107
|
+
this.items.reduce((s, i) => s + i.price, 0) * (1 - this.discount),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
add(item: { name: string; price: number }) {
|
|
111
|
+
this.items = [...this.items, item];
|
|
112
|
+
}
|
|
19
113
|
}
|
|
20
114
|
|
|
21
|
-
|
|
115
|
+
export const cart = new CartStore();
|
|
22
116
|
```
|
|
117
|
+
|
|
118
|
+
Stores are **framework-agnostic** — the same instance drives a custom element, a React component, and a plain effect in sync.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## JSX → DOM
|
|
123
|
+
|
|
124
|
+
JSX compiles directly to `document.createElement`. No virtual DOM, no diffing.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
// This:
|
|
128
|
+
const el = <button onClick={() => count(count() + 1)}>{count}</button>;
|
|
129
|
+
|
|
130
|
+
// Is equivalent to:
|
|
131
|
+
const el = document.createElement("button");
|
|
132
|
+
el.addEventListener("click", () => count(count() + 1));
|
|
133
|
+
// `count` signal creates a live text node — updates in place on change
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Passing a signal or `() => T` as a child or prop creates a **live binding** — the DOM updates in place, never re-rendering the surrounding tree.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
const name = signal("Alice");
|
|
140
|
+
|
|
141
|
+
<p>Hello, {name}!</p> // live text node
|
|
142
|
+
<input value={name} /> // live attribute
|
|
143
|
+
<div class:active={computed(() => name() !== "")} /> // reactive class
|
|
144
|
+
<span style:color={signal("red")} /> // reactive style
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Prop namespaces
|
|
148
|
+
|
|
149
|
+
| Syntax | Effect |
|
|
150
|
+
|--------|--------|
|
|
151
|
+
| `{signal}` / `{() => fn()}` | Live-bound reactive child |
|
|
152
|
+
| `onClick={fn}` | Event listener (camelCase → `onclick`) |
|
|
153
|
+
| `on:click={fn}` | Explicit event namespace |
|
|
154
|
+
| `class:active={bool}` | Reactive `classList.toggle` |
|
|
155
|
+
| `style:color={value}` | Reactive inline style property |
|
|
156
|
+
| `prop:foo={val}` | Force property assignment (skips `setAttribute`) |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Class Components
|
|
161
|
+
|
|
162
|
+
Any class with a `render()` method returning an `Element` is a component. Components own their state and produce elements.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { reactive, computed } from "elements-kit/signals";
|
|
166
|
+
|
|
167
|
+
class Counter {
|
|
168
|
+
@reactive() count = 0;
|
|
169
|
+
doubled = computed(() => this.count * 2);
|
|
170
|
+
|
|
171
|
+
render() {
|
|
172
|
+
return (
|
|
173
|
+
<section>
|
|
174
|
+
<p>{() => this.count} × 2 = {this.doubled}</p>
|
|
175
|
+
<button onClick={() => this.count++}>+1</button>
|
|
176
|
+
</section>
|
|
177
|
+
) as Element;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
document.getElementById("app")!.appendChild(new Counter().render());
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Custom Elements
|
|
187
|
+
|
|
188
|
+
ElementsKit enhances native `HTMLElement` subclasses — start with the platform, add only what you need.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { reactive, computed } from "elements-kit/signals";
|
|
192
|
+
import { attributes, ATTRIBUTES as attr } from "elements-kit/attributes";
|
|
193
|
+
|
|
194
|
+
@attributes
|
|
195
|
+
class CounterElement extends HTMLElement {
|
|
196
|
+
static [attr] = {
|
|
197
|
+
count(this: CounterElement, value: string | null) {
|
|
198
|
+
this.count = Number(value ?? 0);
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
@reactive() count = 0;
|
|
203
|
+
doubled = computed(() => this.count * 2);
|
|
204
|
+
|
|
205
|
+
connectedCallback() {
|
|
206
|
+
this.appendChild(
|
|
207
|
+
<section>
|
|
208
|
+
<p>{() => this.count} × 2 = {this.doubled}</p>
|
|
209
|
+
<button onClick={() => this.count++}>+1</button>
|
|
210
|
+
</section> as Element,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
customElements.define("x-counter", CounterElement);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`<x-counter count="5" />` — attribute bound, reactive, works in any HTML context.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## React Integration
|
|
223
|
+
|
|
224
|
+
Connect signals and stores to React components via `useSyncExternalStore`:
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import { useSignal, useScope } from "elements-kit/signals/react";
|
|
228
|
+
import { cart } from "./cart-store";
|
|
229
|
+
|
|
230
|
+
function CartSummary() {
|
|
231
|
+
// Reads a @reactive field — re-renders only when cart.items changes
|
|
232
|
+
const items = useSignal(() => cart.items);
|
|
233
|
+
const total = useSignal(cart.total); // Computed<T> works directly
|
|
234
|
+
|
|
235
|
+
// Effects tied to this component's lifetime
|
|
236
|
+
useScope(() => {
|
|
237
|
+
effect(() => console.log("cart updated:", items));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return <p>{items.length} items — ${total.toFixed(2)}</p>;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The same `cart` store drives custom elements, React trees, and plain scripts — all in sync.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Signal Helpers
|
|
249
|
+
|
|
250
|
+
Pre-built signal factories for common browser APIs:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { createMediaSignal } from "elements-kit/signals/media";
|
|
254
|
+
|
|
255
|
+
const isDark = createMediaSignal("(prefers-color-scheme: dark)");
|
|
256
|
+
const isMobile = createMediaSignal("(max-width: 640px)");
|
|
257
|
+
|
|
258
|
+
effect(() => document.documentElement.classList.toggle("dark", isDark()));
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## `For` — Keyed List Rendering
|
|
264
|
+
|
|
265
|
+
Reconciles a reactive array into the DOM. Each item renders once per key — no full re-renders on reorder, add, or remove.
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
import { For } from "elements-kit";
|
|
269
|
+
|
|
270
|
+
<ul>
|
|
271
|
+
<For each={todos} by={(todo) => todo.id}>
|
|
272
|
+
{(todo) => (
|
|
273
|
+
<li>
|
|
274
|
+
<input type="checkbox" checked={computed(() => todo.done)} on:change={() => (todo.done = !todo.done)} />
|
|
275
|
+
{todo.text}
|
|
276
|
+
</li>
|
|
277
|
+
)}
|
|
278
|
+
</For>
|
|
279
|
+
</ul>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## `@reactive()` Decorator
|
|
285
|
+
|
|
286
|
+
Makes any class field reactive — reads subscribe, writes trigger updates.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { reactive, computed } from "elements-kit/signals";
|
|
290
|
+
|
|
291
|
+
class TodoApp {
|
|
292
|
+
@reactive() todos: Todo[] = [];
|
|
293
|
+
@reactive() showDone = true;
|
|
294
|
+
|
|
295
|
+
visible = computed(() =>
|
|
296
|
+
this.showDone ? this.todos : this.todos.filter((t) => !t.done),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## `@attributes` Decorator
|
|
304
|
+
|
|
305
|
+
Wires `observedAttributes` and `attributeChangedCallback` from a static map:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { attributes, ATTRIBUTES as attr } from "elements-kit/attributes";
|
|
309
|
+
|
|
310
|
+
@attributes
|
|
311
|
+
class MyElement extends HTMLElement {
|
|
312
|
+
static [attr] = {
|
|
313
|
+
value(this: MyElement, v: string | null) {
|
|
314
|
+
this.value = v ?? "";
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
@reactive() value = "";
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Roadmap
|
|
325
|
+
|
|
326
|
+
- [ ] More signal helpers (`localStorage`, `IntersectionObserver`, `ResizeObserver`, …)
|
|
327
|
+
- [ ] Context — share state across a subtree without prop drilling
|
|
328
|
+
- [ ] Async signal — `signal.from(promise)`, `signal.from(observable)`
|
|
329
|
+
- [ ] UI library — pre-built reactive components built on ElementsKit primitives
|
|
330
|
+
- [ ] More framework integrations (Vue, Solid, Angular, …)
|
|
331
|
+
- [ ] Tutorial — building a full app from scratch
|
|
332
|
+
- [ ] Complete TypeScript strict-mode coverage
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//#region src/attributes.d.ts
|
|
2
|
+
interface AttrChangeHandler<T> {
|
|
3
|
+
(this: T, value: string | null, oldValue?: string | null): void;
|
|
4
|
+
}
|
|
5
|
+
declare const ATTRIBUTES: unique symbol;
|
|
6
|
+
/**
|
|
7
|
+
* Dispatches an attribute change to the matching handler in the static `attributes` map,
|
|
8
|
+
* walking the prototype chain for inherited handlers.
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* class MyElement extends HTMLElement {
|
|
12
|
+
* static [ATTRIBUTES]: Attributes<MyElement> = {
|
|
13
|
+
* count(value) {
|
|
14
|
+
* this.#count = Number(value);
|
|
15
|
+
* },
|
|
16
|
+
* };
|
|
17
|
+
* static observedAttributes: string[] = observedAttributes(MyElement);
|
|
18
|
+
*
|
|
19
|
+
* attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
20
|
+
* dispatchAttrChange.call(this, name, oldValue, newValue);
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
declare function dispatchAttrChange<T extends {
|
|
26
|
+
constructor: {
|
|
27
|
+
[ATTRIBUTES]: Record<string, AttrChangeHandler<T>>;
|
|
28
|
+
};
|
|
29
|
+
}>(this: T, name: string, oldValue: string | null, newValue: string | null): void;
|
|
30
|
+
type Attributes<T> = Record<string, AttrChangeHandler<T>>;
|
|
31
|
+
/**
|
|
32
|
+
* Returns a deduplicated array of all observed attribute names for a custom element class and its ancestors.
|
|
33
|
+
*
|
|
34
|
+
* Call after defining static `[ATTRIBUTES]`, and assign to static `observedAttributes`.
|
|
35
|
+
*
|
|
36
|
+
* Example:
|
|
37
|
+
* ```ts
|
|
38
|
+
* class MyElement extends HTMLElement {
|
|
39
|
+
* static [ATTRIBUTES]: Attributes<MyElement> = {
|
|
40
|
+
* count(value) {
|
|
41
|
+
* this.#count = Number(value);
|
|
42
|
+
* },
|
|
43
|
+
* };
|
|
44
|
+
* static observedAttributes: string[] = observedAttributes(MyElement);
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* class ChildElement extends MyElement {
|
|
48
|
+
* static [ATTRIBUTES]: Attributes<ChildElement> = {
|
|
49
|
+
* bar(value) {
|
|
50
|
+
* // ...
|
|
51
|
+
* },
|
|
52
|
+
* };
|
|
53
|
+
* static observedAttributes: string[] = observedAttributes(ChildElement);
|
|
54
|
+
* }
|
|
55
|
+
* // ChildElement.observedAttributes will include both 'count' and 'bar', deduplicated.
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @param cls The custom element class constructor
|
|
59
|
+
* @returns Array of unique attribute names to observe
|
|
60
|
+
*/
|
|
61
|
+
declare function observedAttributes(cls: any): string[];
|
|
62
|
+
/**
|
|
63
|
+
* A class decorator that automatically wires up `observedAttributes` and `attributeChangedCallback`
|
|
64
|
+
* from a static `[ATTRIBUTES]` map.
|
|
65
|
+
*
|
|
66
|
+
* The `this` type inside attribute handlers is automatically inferred from the decorated class.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* @attributes
|
|
71
|
+
* class MyElement extends HTMLElement {
|
|
72
|
+
* static [ATTRIBUTES] = {
|
|
73
|
+
* count(this: MyElement, value: string | null) {
|
|
74
|
+
* this.count = Number(value);
|
|
75
|
+
* },
|
|
76
|
+
* };
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
type AttributeTarget<T extends abstract new (...args: any[]) => HTMLElement> = T & {
|
|
81
|
+
[ATTRIBUTES]: Record<string, AttrChangeHandler<InstanceType<T>>>;
|
|
82
|
+
};
|
|
83
|
+
type AttributeDecorated<T extends abstract new (...args: any[]) => HTMLElement> = T & (new (...args: any[]) => InstanceType<T> & {
|
|
84
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
85
|
+
}) & {
|
|
86
|
+
observedAttributes: string[];
|
|
87
|
+
};
|
|
88
|
+
declare function attributes<T extends abstract new (...args: any[]) => HTMLElement>(target: AttributeTarget<T>, context: ClassDecoratorContext<T>): AttributeDecorated<T>;
|
|
89
|
+
//#endregion
|
|
90
|
+
export { ATTRIBUTES, AttrChangeHandler, AttributeDecorated, AttributeTarget, Attributes, attributes, dispatchAttrChange, observedAttributes };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/attributes.ts
|
|
2
|
+
const ATTRIBUTES = Symbol("attributes");
|
|
3
|
+
/**
|
|
4
|
+
* Dispatches an attribute change to the matching handler in the static `attributes` map,
|
|
5
|
+
* walking the prototype chain for inherited handlers.
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* class MyElement extends HTMLElement {
|
|
9
|
+
* static [ATTRIBUTES]: Attributes<MyElement> = {
|
|
10
|
+
* count(value) {
|
|
11
|
+
* this.#count = Number(value);
|
|
12
|
+
* },
|
|
13
|
+
* };
|
|
14
|
+
* static observedAttributes: string[] = observedAttributes(MyElement);
|
|
15
|
+
*
|
|
16
|
+
* attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
|
|
17
|
+
* dispatchAttrChange.call(this, name, oldValue, newValue);
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function dispatchAttrChange(name, oldValue, newValue) {
|
|
23
|
+
let cls = this.constructor;
|
|
24
|
+
while (cls) {
|
|
25
|
+
if (cls[ATTRIBUTES] && name in cls[ATTRIBUTES]) {
|
|
26
|
+
cls[ATTRIBUTES][name].call(this, newValue, oldValue);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
cls = Object.getPrototypeOf(cls);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns a deduplicated array of all observed attribute names for a custom element class and its ancestors.
|
|
34
|
+
*
|
|
35
|
+
* Call after defining static `[ATTRIBUTES]`, and assign to static `observedAttributes`.
|
|
36
|
+
*
|
|
37
|
+
* Example:
|
|
38
|
+
* ```ts
|
|
39
|
+
* class MyElement extends HTMLElement {
|
|
40
|
+
* static [ATTRIBUTES]: Attributes<MyElement> = {
|
|
41
|
+
* count(value) {
|
|
42
|
+
* this.#count = Number(value);
|
|
43
|
+
* },
|
|
44
|
+
* };
|
|
45
|
+
* static observedAttributes: string[] = observedAttributes(MyElement);
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* class ChildElement extends MyElement {
|
|
49
|
+
* static [ATTRIBUTES]: Attributes<ChildElement> = {
|
|
50
|
+
* bar(value) {
|
|
51
|
+
* // ...
|
|
52
|
+
* },
|
|
53
|
+
* };
|
|
54
|
+
* static observedAttributes: string[] = observedAttributes(ChildElement);
|
|
55
|
+
* }
|
|
56
|
+
* // ChildElement.observedAttributes will include both 'count' and 'bar', deduplicated.
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @param cls The custom element class constructor
|
|
60
|
+
* @returns Array of unique attribute names to observe
|
|
61
|
+
*/
|
|
62
|
+
function observedAttributes(cls) {
|
|
63
|
+
const s = new Set(Object.keys(cls[ATTRIBUTES] || {}));
|
|
64
|
+
let _cls = Object.getPrototypeOf(cls);
|
|
65
|
+
while (_cls) {
|
|
66
|
+
if (_cls.observedAttributes) _cls.observedAttributes.forEach((attr) => s.add(attr));
|
|
67
|
+
_cls = Object.getPrototypeOf(_cls);
|
|
68
|
+
}
|
|
69
|
+
return Array.from(s);
|
|
70
|
+
}
|
|
71
|
+
function attributes(target, context) {
|
|
72
|
+
context.addInitializer(function() {
|
|
73
|
+
target.observedAttributes = observedAttributes(target);
|
|
74
|
+
});
|
|
75
|
+
target.prototype.attributeChangedCallback = function(name, oldValue, newValue) {
|
|
76
|
+
dispatchAttrChange.call(this, name, oldValue, newValue);
|
|
77
|
+
};
|
|
78
|
+
return target;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { ATTRIBUTES, attributes, dispatchAttrChange, observedAttributes };
|