@radishland/runtime 0.0.7 → 1.0.0-alpha.1
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/build.ts +14 -0
- package/client/config.js +12 -0
- package/client/handler-registry.js +236 -0
- package/client/handlers.js +174 -0
- package/client/index.js +3 -380
- package/client/reactivity.js +162 -0
- package/client/types.d.js +1 -0
- package/client/utils.js +65 -0
- package/deno.json +21 -0
- package/package.json +2 -21
- package/src/config.ts +12 -0
- package/src/handler-registry.ts +274 -0
- package/src/handlers.ts +174 -0
- package/src/index.ts +3 -0
- package/src/reactivity.ts +179 -0
- package/src/types.d.ts +75 -0
- package/src/utils.ts +71 -0
- package/README.md +0 -3
- package/client/index.d.ts +0 -89
@@ -0,0 +1,274 @@
|
|
1
|
+
import { booleanAttributes } from "./utils.ts";
|
2
|
+
import { bindingConfig } from "./config.ts";
|
3
|
+
|
4
|
+
import { $effect, getValue, isComputed, isState } from "./reactivity.ts";
|
5
|
+
import type {
|
6
|
+
AttrRequestDetail,
|
7
|
+
AutonomousCustomElement,
|
8
|
+
BindRequestDetail,
|
9
|
+
ClassRequestDetail,
|
10
|
+
Destructor,
|
11
|
+
EffectCallback,
|
12
|
+
EffectOptions,
|
13
|
+
HTMLRequestDetail,
|
14
|
+
OnRequestDetail,
|
15
|
+
PropRequestDetail,
|
16
|
+
TextRequestDetail,
|
17
|
+
UseRequestDetail,
|
18
|
+
} from "./types.d.ts";
|
19
|
+
|
20
|
+
/**
|
21
|
+
* A Scoped Handler Registry implements the logic for handling effect requests.
|
22
|
+
*
|
23
|
+
* Extend this class by adding new methods in your subclass to implement your own effect handlers
|
24
|
+
*/
|
25
|
+
export class HandlerRegistry extends HTMLElement
|
26
|
+
implements AutonomousCustomElement {
|
27
|
+
[key: string]: any;
|
28
|
+
|
29
|
+
#cleanup: Destructor[] = [];
|
30
|
+
|
31
|
+
/**
|
32
|
+
* References the handler's `AbortController`.
|
33
|
+
*
|
34
|
+
* The `abort` method of the controller is called in the `disconnectedCallback` method. It allows to cleanup event handlers and other abortable operations
|
35
|
+
*/
|
36
|
+
abortController: AbortController;
|
37
|
+
|
38
|
+
constructor() {
|
39
|
+
super();
|
40
|
+
this.abortController = new AbortController();
|
41
|
+
}
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Creates an effect that is automatically cleaned up when the component is disconnected
|
45
|
+
*
|
46
|
+
* An optional AbortSignal can be provided to abort the effect prematurely
|
47
|
+
*/
|
48
|
+
$effect(callback: EffectCallback, options?: EffectOptions) {
|
49
|
+
const signals = [this.abortController.signal];
|
50
|
+
if (options?.signal) signals.push(options.signal);
|
51
|
+
|
52
|
+
$effect(callback, { ...options, signal: AbortSignal.any(signals) });
|
53
|
+
}
|
54
|
+
|
55
|
+
#get(identifier: string) {
|
56
|
+
return this[identifier];
|
57
|
+
}
|
58
|
+
|
59
|
+
#handleOn(e: Event) {
|
60
|
+
if (e instanceof CustomEvent) {
|
61
|
+
const { handler, type }: OnRequestDetail = e.detail;
|
62
|
+
|
63
|
+
if (handler in this && typeof this.#get(handler) === "function") {
|
64
|
+
e.target?.addEventListener(type, this.#get(handler).bind(this));
|
65
|
+
e.stopPropagation();
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
#handleClass(e: Event) {
|
71
|
+
const target = e.target;
|
72
|
+
if (e instanceof CustomEvent && target) {
|
73
|
+
const { identifier }: ClassRequestDetail = e.detail;
|
74
|
+
|
75
|
+
if (identifier in this) {
|
76
|
+
this.$effect(() => {
|
77
|
+
const classList = getValue(this.#get(identifier));
|
78
|
+
if (classList && typeof classList === "object") {
|
79
|
+
for (const [k, v] of Object.entries(classList)) {
|
80
|
+
const force = !!getValue(v);
|
81
|
+
for (const className of k.split(" ")) {
|
82
|
+
// @ts-ignore target is an HTMLElement
|
83
|
+
target.classList.toggle(
|
84
|
+
className,
|
85
|
+
force,
|
86
|
+
);
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
});
|
91
|
+
|
92
|
+
e.stopPropagation();
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
#handleUse(e: Event) {
|
98
|
+
if (e instanceof CustomEvent) {
|
99
|
+
const { hook }: UseRequestDetail = e.detail;
|
100
|
+
|
101
|
+
if (hook in this && typeof this.#get(hook) === "function") {
|
102
|
+
const cleanup = this.#get(hook).bind(this)(e.target);
|
103
|
+
if (typeof cleanup === "function") {
|
104
|
+
this.#cleanup.push(cleanup);
|
105
|
+
}
|
106
|
+
e.stopPropagation();
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
#handleAttr(e: Event) {
|
112
|
+
if (e instanceof CustomEvent) {
|
113
|
+
const { identifier, attribute }: AttrRequestDetail = e.detail;
|
114
|
+
const target = e.target;
|
115
|
+
|
116
|
+
if (
|
117
|
+
identifier in this && target instanceof HTMLElement &&
|
118
|
+
attribute in target
|
119
|
+
) {
|
120
|
+
const ref = this.#get(identifier);
|
121
|
+
|
122
|
+
const setAttr = () => {
|
123
|
+
const value = getValue(ref);
|
124
|
+
if (booleanAttributes.includes(attribute)) {
|
125
|
+
value
|
126
|
+
? target.setAttribute(attribute, "")
|
127
|
+
: target.removeAttribute(attribute);
|
128
|
+
} else {
|
129
|
+
target.setAttribute(attribute, `${value}`);
|
130
|
+
}
|
131
|
+
};
|
132
|
+
|
133
|
+
if (isState(ref) || isComputed(ref)) {
|
134
|
+
this.$effect(() => setAttr());
|
135
|
+
} else {
|
136
|
+
setAttr();
|
137
|
+
}
|
138
|
+
|
139
|
+
e.stopPropagation();
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
#handleProp(e: Event) {
|
145
|
+
if (e instanceof CustomEvent) {
|
146
|
+
const { identifier, property }: PropRequestDetail = e.detail;
|
147
|
+
const target = e.target;
|
148
|
+
|
149
|
+
if (identifier in this && target && property in target) {
|
150
|
+
const ref = this.#get(identifier);
|
151
|
+
|
152
|
+
const setProp = () => {
|
153
|
+
const value = getValue(ref);
|
154
|
+
// @ts-ignore property is in target
|
155
|
+
target[property] = value;
|
156
|
+
};
|
157
|
+
|
158
|
+
if (isState(ref) || isComputed(ref)) {
|
159
|
+
this.$effect(() => setProp());
|
160
|
+
} else {
|
161
|
+
setProp();
|
162
|
+
}
|
163
|
+
|
164
|
+
e.stopPropagation();
|
165
|
+
}
|
166
|
+
}
|
167
|
+
}
|
168
|
+
|
169
|
+
#handleText(e: Event) {
|
170
|
+
if (e instanceof CustomEvent) {
|
171
|
+
const target = e.target;
|
172
|
+
|
173
|
+
const { identifier }: TextRequestDetail = e.detail;
|
174
|
+
|
175
|
+
if (identifier in this && target instanceof HTMLElement) {
|
176
|
+
const ref = this.#get(identifier);
|
177
|
+
|
178
|
+
const setTextContent = () => {
|
179
|
+
const value = getValue(ref);
|
180
|
+
target.textContent = `${value}`;
|
181
|
+
};
|
182
|
+
|
183
|
+
if (isState(ref) || isComputed(ref)) {
|
184
|
+
this.$effect(() => setTextContent());
|
185
|
+
} else {
|
186
|
+
setTextContent();
|
187
|
+
}
|
188
|
+
|
189
|
+
e.stopPropagation();
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
#handleHTML(e: Event) {
|
195
|
+
if (e instanceof CustomEvent) {
|
196
|
+
const { identifier }: HTMLRequestDetail = e.detail;
|
197
|
+
const target = e.target;
|
198
|
+
|
199
|
+
if (identifier in this && target instanceof HTMLElement) {
|
200
|
+
const ref = this.#get(identifier);
|
201
|
+
|
202
|
+
const setInnerHTML = () => {
|
203
|
+
const value = getValue(ref);
|
204
|
+
target.innerHTML = `${value}`;
|
205
|
+
};
|
206
|
+
|
207
|
+
if (isState(ref) || isComputed(ref)) {
|
208
|
+
this.$effect(() => setInnerHTML());
|
209
|
+
} else {
|
210
|
+
setInnerHTML();
|
211
|
+
}
|
212
|
+
|
213
|
+
e.stopPropagation();
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
#handleBind(e: Event) {
|
219
|
+
if (e instanceof CustomEvent) {
|
220
|
+
const { identifier, property }: BindRequestDetail = e.detail;
|
221
|
+
const target = e.target;
|
222
|
+
|
223
|
+
if (
|
224
|
+
identifier in this && target instanceof HTMLElement &&
|
225
|
+
property in target
|
226
|
+
) {
|
227
|
+
const state = this.#get(identifier);
|
228
|
+
if (isState(state)) {
|
229
|
+
// @ts-ignore property is in target
|
230
|
+
state.value = target[property];
|
231
|
+
|
232
|
+
// Add change listener
|
233
|
+
target.addEventListener(bindingConfig[property].event, () => {
|
234
|
+
// @ts-ignore property is in target
|
235
|
+
state.value = target[property];
|
236
|
+
});
|
237
|
+
|
238
|
+
// Sync
|
239
|
+
this.$effect(() => {
|
240
|
+
// @ts-ignore property is in target
|
241
|
+
target[property] = state.value;
|
242
|
+
});
|
243
|
+
}
|
244
|
+
|
245
|
+
e.stopPropagation();
|
246
|
+
}
|
247
|
+
}
|
248
|
+
}
|
249
|
+
|
250
|
+
connectedCallback() {
|
251
|
+
const { signal } = this.abortController;
|
252
|
+
|
253
|
+
this.addEventListener("@attr-request", this.#handleAttr, { signal });
|
254
|
+
this.addEventListener("@class-request", this.#handleClass, { signal });
|
255
|
+
this.addEventListener("@on-request", this.#handleOn, { signal });
|
256
|
+
this.addEventListener("@use-request", this.#handleUse, { signal });
|
257
|
+
this.addEventListener("@prop-request", this.#handleProp, { signal });
|
258
|
+
this.addEventListener("@html-request", this.#handleHTML, { signal });
|
259
|
+
this.addEventListener("@text-request", this.#handleText, { signal });
|
260
|
+
this.addEventListener("@bind-request", this.#handleBind, { signal });
|
261
|
+
}
|
262
|
+
|
263
|
+
disconnectedCallback() {
|
264
|
+
this.abortController.abort();
|
265
|
+
|
266
|
+
for (const cleanup of this.#cleanup) {
|
267
|
+
cleanup();
|
268
|
+
}
|
269
|
+
}
|
270
|
+
}
|
271
|
+
|
272
|
+
if (window && !customElements.get("handler-registry")) {
|
273
|
+
customElements.define("handler-registry", HandlerRegistry);
|
274
|
+
}
|
package/src/handlers.ts
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
import { bindingConfig } from "./config.ts";
|
2
|
+
import { spaces_sep_by_comma } from "./utils.ts";
|
3
|
+
|
4
|
+
const bindingsQueryString = Object.keys(bindingConfig).map((property) =>
|
5
|
+
`[\\@bind\\:${property}]`
|
6
|
+
).join(",");
|
7
|
+
|
8
|
+
setTimeout(() => {
|
9
|
+
customElements?.whenDefined("handler-registry").then(() => {
|
10
|
+
document.querySelectorAll(
|
11
|
+
`[\\@on],[\\@use],[\\@attr],[\\@attr\\|client],[\\@prop],${bindingsQueryString},[\\@text],[\\@html]`,
|
12
|
+
)
|
13
|
+
.forEach(
|
14
|
+
(entry) => {
|
15
|
+
const events = entry.getAttribute("@on")?.trim()
|
16
|
+
?.split(spaces_sep_by_comma);
|
17
|
+
|
18
|
+
if (events) {
|
19
|
+
for (const event of events) {
|
20
|
+
const [type, handler] = event.split(":");
|
21
|
+
|
22
|
+
const onRequest = new CustomEvent("@on-request", {
|
23
|
+
bubbles: true,
|
24
|
+
cancelable: true,
|
25
|
+
composed: true,
|
26
|
+
detail: {
|
27
|
+
type,
|
28
|
+
handler: handler || type,
|
29
|
+
},
|
30
|
+
});
|
31
|
+
|
32
|
+
entry.dispatchEvent(onRequest);
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
const hooks = entry.getAttribute("@use")?.trim()
|
37
|
+
?.split(spaces_sep_by_comma);
|
38
|
+
|
39
|
+
if (hooks) {
|
40
|
+
for (const hook of hooks) {
|
41
|
+
const useRequest = new CustomEvent("@use-request", {
|
42
|
+
bubbles: true,
|
43
|
+
cancelable: true,
|
44
|
+
composed: true,
|
45
|
+
detail: {
|
46
|
+
hook,
|
47
|
+
},
|
48
|
+
});
|
49
|
+
|
50
|
+
entry.dispatchEvent(useRequest);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
const props = (entry.getAttribute("@prop"))?.trim()
|
55
|
+
.split(spaces_sep_by_comma);
|
56
|
+
|
57
|
+
if (props) {
|
58
|
+
for (const prop of props) {
|
59
|
+
const [key, value] = prop.split(":");
|
60
|
+
|
61
|
+
const propRequest = new CustomEvent("@prop-request", {
|
62
|
+
bubbles: true,
|
63
|
+
cancelable: true,
|
64
|
+
composed: true,
|
65
|
+
detail: {
|
66
|
+
property: key,
|
67
|
+
identifier: value || key,
|
68
|
+
},
|
69
|
+
});
|
70
|
+
|
71
|
+
entry.dispatchEvent(propRequest);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
const text = entry.hasAttribute("@text");
|
76
|
+
|
77
|
+
if (text) {
|
78
|
+
const identifier = entry.getAttribute("@text") || "text";
|
79
|
+
|
80
|
+
const textRequest = new CustomEvent("@text-request", {
|
81
|
+
bubbles: true,
|
82
|
+
cancelable: true,
|
83
|
+
composed: true,
|
84
|
+
detail: {
|
85
|
+
identifier,
|
86
|
+
},
|
87
|
+
});
|
88
|
+
|
89
|
+
entry.dispatchEvent(textRequest);
|
90
|
+
}
|
91
|
+
|
92
|
+
const html = entry.hasAttribute("@html");
|
93
|
+
|
94
|
+
if (html) {
|
95
|
+
const identifier = entry.getAttribute("@html") || "html";
|
96
|
+
|
97
|
+
const htmlRequest = new CustomEvent("@html-request", {
|
98
|
+
bubbles: true,
|
99
|
+
cancelable: true,
|
100
|
+
composed: true,
|
101
|
+
detail: {
|
102
|
+
identifier,
|
103
|
+
},
|
104
|
+
});
|
105
|
+
|
106
|
+
entry.dispatchEvent(htmlRequest);
|
107
|
+
}
|
108
|
+
|
109
|
+
const classList = entry.hasAttribute("@class");
|
110
|
+
|
111
|
+
if (classList) {
|
112
|
+
const identifier = entry.getAttribute("@class") || "class";
|
113
|
+
|
114
|
+
const classRequest = new CustomEvent("@class-request", {
|
115
|
+
bubbles: true,
|
116
|
+
cancelable: true,
|
117
|
+
composed: true,
|
118
|
+
detail: {
|
119
|
+
identifier,
|
120
|
+
},
|
121
|
+
});
|
122
|
+
|
123
|
+
entry.dispatchEvent(classRequest);
|
124
|
+
}
|
125
|
+
|
126
|
+
const attributes = [
|
127
|
+
...(entry.getAttribute("@attr"))?.trim()
|
128
|
+
.split(spaces_sep_by_comma) ?? [],
|
129
|
+
...(entry.getAttribute("@attr|client"))?.trim()
|
130
|
+
.split(spaces_sep_by_comma) ?? [],
|
131
|
+
];
|
132
|
+
|
133
|
+
if (attributes.length > 0) {
|
134
|
+
for (const attribute of attributes) {
|
135
|
+
const [key, value] = attribute.split(":");
|
136
|
+
|
137
|
+
const attrRequest = new CustomEvent("@attr-request", {
|
138
|
+
bubbles: true,
|
139
|
+
cancelable: true,
|
140
|
+
composed: true,
|
141
|
+
detail: {
|
142
|
+
attribute: key,
|
143
|
+
identifier: value || key,
|
144
|
+
},
|
145
|
+
});
|
146
|
+
|
147
|
+
entry.dispatchEvent(attrRequest);
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
for (const property of Object.keys(bindingConfig)) {
|
152
|
+
if (entry.hasAttribute(`@bind:${property}`)) {
|
153
|
+
const identifier =
|
154
|
+
entry.getAttribute(`@bind:${property}`)?.trim() ||
|
155
|
+
property;
|
156
|
+
|
157
|
+
const bindRequest = new CustomEvent("@bind-request", {
|
158
|
+
bubbles: true,
|
159
|
+
cancelable: true,
|
160
|
+
composed: true,
|
161
|
+
detail: {
|
162
|
+
property,
|
163
|
+
identifier,
|
164
|
+
handled: false,
|
165
|
+
},
|
166
|
+
});
|
167
|
+
|
168
|
+
entry.dispatchEvent(bindRequest);
|
169
|
+
}
|
170
|
+
}
|
171
|
+
},
|
172
|
+
);
|
173
|
+
});
|
174
|
+
}, 100);
|
package/src/index.ts
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
import { Signal } from "signal-polyfill";
|
2
|
+
import type {
|
3
|
+
Destructor,
|
4
|
+
EffectCallback,
|
5
|
+
EffectOptions,
|
6
|
+
ReactivityOptions,
|
7
|
+
} from "./types.d.ts";
|
8
|
+
|
9
|
+
// @ts-ignore we're hiding get and set
|
10
|
+
export class ReactiveValue<T> extends Signal.State<T> {
|
11
|
+
// @ts-ignore see above
|
12
|
+
private override get;
|
13
|
+
// @ts-ignore see above
|
14
|
+
private override set;
|
15
|
+
|
16
|
+
get value(): T {
|
17
|
+
return super.get();
|
18
|
+
}
|
19
|
+
|
20
|
+
set value(newValue: T) {
|
21
|
+
super.set(newValue);
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
// @ts-ignore we're hiding get and set
|
26
|
+
export class ReactiveComputation<T> extends Signal.Computed<T> {
|
27
|
+
// @ts-ignore see above
|
28
|
+
private override get;
|
29
|
+
|
30
|
+
get value(): T {
|
31
|
+
return super.get();
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
const maybeReactiveObjectType = <T>(thing: T, options: ReactivityOptions) => {
|
36
|
+
if (typeof thing === "object") {
|
37
|
+
if (Array.isArray(thing)) {
|
38
|
+
return $array(thing, options);
|
39
|
+
} else if (thing) {
|
40
|
+
return $object(thing, options);
|
41
|
+
}
|
42
|
+
}
|
43
|
+
return thing;
|
44
|
+
};
|
45
|
+
|
46
|
+
export const $object = <T extends Record<PropertyKey, any>>(
|
47
|
+
init: T,
|
48
|
+
options: ReactivityOptions = { deep: false },
|
49
|
+
): T => {
|
50
|
+
if (options.deep === true) {
|
51
|
+
for (const [key, value] of Object.entries(init)) {
|
52
|
+
init[key as keyof T] = maybeReactiveObjectType(value, options);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
const state = new Signal.State(init);
|
56
|
+
|
57
|
+
const proxy = new Proxy(init, {
|
58
|
+
get(_target, p, _receiver) {
|
59
|
+
return state.get()[p];
|
60
|
+
},
|
61
|
+
set(_target, p, newValue, _receiver) {
|
62
|
+
state.set({
|
63
|
+
...state.get(),
|
64
|
+
[p]: maybeReactiveObjectType(newValue, options),
|
65
|
+
});
|
66
|
+
return true;
|
67
|
+
},
|
68
|
+
});
|
69
|
+
|
70
|
+
return proxy;
|
71
|
+
};
|
72
|
+
|
73
|
+
export const $array = <T extends ArrayLike<any>>(
|
74
|
+
init: T,
|
75
|
+
options: ReactivityOptions = { deep: false },
|
76
|
+
): T => {
|
77
|
+
if (options.deep) {
|
78
|
+
for (const [key, value] of Object.entries(init)) {
|
79
|
+
init[key as keyof T] = maybeReactiveObjectType(value, options);
|
80
|
+
}
|
81
|
+
}
|
82
|
+
const state = new Signal.State(init);
|
83
|
+
|
84
|
+
const proxy = new Proxy(init, {
|
85
|
+
get(_target, p, _receiver) {
|
86
|
+
// @ts-ignore state has p
|
87
|
+
return state.get()[p];
|
88
|
+
},
|
89
|
+
set(_target, p, newValue, _receiver) {
|
90
|
+
state.set({
|
91
|
+
...state.get(),
|
92
|
+
[p]: maybeReactiveObjectType(newValue, options),
|
93
|
+
});
|
94
|
+
return true;
|
95
|
+
},
|
96
|
+
});
|
97
|
+
|
98
|
+
return proxy;
|
99
|
+
};
|
100
|
+
|
101
|
+
export const isState = (
|
102
|
+
s: unknown,
|
103
|
+
): s is InstanceType<typeof ReactiveValue> => {
|
104
|
+
return Signal.isState(s);
|
105
|
+
};
|
106
|
+
|
107
|
+
export const isComputed = (
|
108
|
+
s: unknown,
|
109
|
+
): s is InstanceType<typeof ReactiveComputation> => {
|
110
|
+
return Signal.isComputed(s);
|
111
|
+
};
|
112
|
+
|
113
|
+
export const getValue = (signal: unknown): unknown => {
|
114
|
+
if (isState(signal) || isComputed(signal)) {
|
115
|
+
return signal.value;
|
116
|
+
}
|
117
|
+
return signal;
|
118
|
+
};
|
119
|
+
|
120
|
+
export const $state = <T>(
|
121
|
+
initialValue: T,
|
122
|
+
options?: Signal.Options<T | undefined>,
|
123
|
+
): ReactiveValue<T> => {
|
124
|
+
return new ReactiveValue(initialValue, options);
|
125
|
+
};
|
126
|
+
|
127
|
+
export const $computed = <T>(
|
128
|
+
computation: () => T,
|
129
|
+
options?: Signal.Options<T>,
|
130
|
+
): ReactiveComputation<T> => {
|
131
|
+
return new ReactiveComputation(computation, options);
|
132
|
+
};
|
133
|
+
|
134
|
+
let pending = false;
|
135
|
+
|
136
|
+
const watcher = new Signal.subtle.Watcher(() => {
|
137
|
+
if (!pending) {
|
138
|
+
pending = true;
|
139
|
+
|
140
|
+
queueMicrotask(() => {
|
141
|
+
pending = false;
|
142
|
+
for (const s of watcher.getPending()) s.get();
|
143
|
+
watcher.watch();
|
144
|
+
});
|
145
|
+
}
|
146
|
+
});
|
147
|
+
|
148
|
+
/**
|
149
|
+
* Create an unowned effect that must be cleanup up manually
|
150
|
+
*
|
151
|
+
* Accept an AbortSignal to abort the effect
|
152
|
+
*/
|
153
|
+
export const $effect = (
|
154
|
+
cb: EffectCallback,
|
155
|
+
options?: EffectOptions,
|
156
|
+
): Destructor => {
|
157
|
+
if (options?.signal?.aborted) return () => {};
|
158
|
+
|
159
|
+
let destroy: Destructor | undefined;
|
160
|
+
const c = new Signal.Computed(() => {
|
161
|
+
destroy?.();
|
162
|
+
destroy = cb() ?? undefined;
|
163
|
+
});
|
164
|
+
watcher.watch(c);
|
165
|
+
c.get();
|
166
|
+
|
167
|
+
let cleaned = false;
|
168
|
+
|
169
|
+
const cleanup = () => {
|
170
|
+
if (cleaned) return;
|
171
|
+
destroy?.();
|
172
|
+
watcher.unwatch(c);
|
173
|
+
cleaned = true;
|
174
|
+
};
|
175
|
+
|
176
|
+
options?.signal.addEventListener("abort", cleanup);
|
177
|
+
|
178
|
+
return cleanup;
|
179
|
+
};
|
package/src/types.d.ts
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
export type OnRequestDetail = {
|
2
|
+
type: string;
|
3
|
+
handler: string;
|
4
|
+
};
|
5
|
+
|
6
|
+
export type UseRequestDetail = {
|
7
|
+
hook: string;
|
8
|
+
};
|
9
|
+
|
10
|
+
export type AttrRequestDetail = {
|
11
|
+
attribute: string;
|
12
|
+
identifier: string;
|
13
|
+
};
|
14
|
+
|
15
|
+
export type PropRequestDetail = {
|
16
|
+
property: string;
|
17
|
+
identifier: string;
|
18
|
+
};
|
19
|
+
|
20
|
+
export type TextRequestDetail = {
|
21
|
+
identifier: string;
|
22
|
+
};
|
23
|
+
|
24
|
+
export type HTMLRequestDetail = {
|
25
|
+
identifier: string;
|
26
|
+
};
|
27
|
+
|
28
|
+
export type ClassRequestDetail = {
|
29
|
+
identifier: string;
|
30
|
+
};
|
31
|
+
|
32
|
+
type BindableProperty = "checked" | "value";
|
33
|
+
|
34
|
+
export type BindRequestDetail = {
|
35
|
+
property: BindableProperty;
|
36
|
+
identifier: string;
|
37
|
+
handled: boolean;
|
38
|
+
};
|
39
|
+
|
40
|
+
export interface AutonomousCustomElement {
|
41
|
+
/**
|
42
|
+
* A static getter
|
43
|
+
*/
|
44
|
+
readonly observedAttributes?: string[] | undefined;
|
45
|
+
/**
|
46
|
+
* A static getter
|
47
|
+
*/
|
48
|
+
readonly disabledFeatures?: ("internals" | "shadow")[] | undefined;
|
49
|
+
/**
|
50
|
+
* A static getter
|
51
|
+
*/
|
52
|
+
readonly formAssociated?: boolean | undefined;
|
53
|
+
|
54
|
+
connectedCallback?(): void;
|
55
|
+
disconnectedCallback?(): void;
|
56
|
+
adoptedCallback?(): void;
|
57
|
+
|
58
|
+
attributeChangedCallback?(
|
59
|
+
name: string,
|
60
|
+
previous: string,
|
61
|
+
next: string,
|
62
|
+
): void;
|
63
|
+
|
64
|
+
formAssociatedCallback?(): void;
|
65
|
+
formResetCallback?(): void;
|
66
|
+
formDisabledCallback?(): void;
|
67
|
+
formStateRestoreCallback?(): void;
|
68
|
+
}
|
69
|
+
|
70
|
+
export type ReactivityOptions = { deep: boolean };
|
71
|
+
export type Destructor = () => void;
|
72
|
+
export type EffectCallback = () => Destructor | void;
|
73
|
+
export type EffectOptions = {
|
74
|
+
signal: AbortSignal;
|
75
|
+
};
|