@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/client/index.js CHANGED
@@ -1,380 +1,3 @@
1
- import { Signal } from 'signal-polyfill';
2
-
3
- // src/utils.ts
4
- var booleanAttributes = [
5
- "allowfullscreen",
6
- // on <iframe>
7
- "async",
8
- // on <script>
9
- "autofocus",
10
- // on <button>, <input>, <select>, <textarea>
11
- "autoplay",
12
- // on <audio>, <video>
13
- "checked",
14
- // on <input type="checkbox">, <input type="radio">
15
- "controls",
16
- // on <audio>, <video>
17
- "default",
18
- // on <track>
19
- "defer",
20
- // on <script>
21
- "disabled",
22
- // on form elements like <button>, <fieldset>, <input>, <optgroup>, <option>,<select>, <textarea>
23
- "formnovalidate",
24
- // on <button>, <input type="submit">
25
- "hidden",
26
- // global
27
- "inert",
28
- // global
29
- "ismap",
30
- // on <img>
31
- "itemscope",
32
- // global; part of microdata
33
- "loop",
34
- // on <audio>, <video>
35
- "multiple",
36
- // on <input type="file">, <select>
37
- "muted",
38
- // on <audio>, <video>
39
- "nomodule",
40
- // on <script>
41
- "novalidate",
42
- // on <form>
43
- "open",
44
- // on <details>
45
- "readonly",
46
- // on <input>, <textarea>
47
- "required",
48
- // on <input>, <select>, <textarea>
49
- "reversed",
50
- // on <ol>
51
- "selected"
52
- // on <option>
53
- ];
54
-
55
- // src/config.ts
56
- var bindingConfig = {
57
- "checked": {
58
- element: ["input"],
59
- type: ["boolean"],
60
- event: "change"
61
- },
62
- "value": {
63
- element: ["input", "select", "textarea"],
64
- type: ["string", "number"],
65
- event: "input"
66
- }
67
- };
68
- var ReactiveValue = class extends Signal.State {
69
- // @ts-ignore see above
70
- get;
71
- // @ts-ignore see above
72
- set;
73
- get value() {
74
- return super.get();
75
- }
76
- set value(newValue) {
77
- super.set(newValue);
78
- }
79
- };
80
- var ReactiveComputation = class extends Signal.Computed {
81
- // @ts-ignore see above
82
- get;
83
- get value() {
84
- return super.get();
85
- }
86
- };
87
- var maybeReactiveObjectType = (thing, options) => {
88
- if (typeof thing === "object") {
89
- if (Array.isArray(thing)) {
90
- return $array(thing, options);
91
- } else if (thing) {
92
- return $object(thing, options);
93
- }
94
- }
95
- return thing;
96
- };
97
- var $object = (init, options = { deep: false }) => {
98
- if (options.deep === true) {
99
- for (const [key, value] of Object.entries(init)) {
100
- init[key] = maybeReactiveObjectType(value, options);
101
- }
102
- }
103
- const state = new Signal.State(init);
104
- const proxy = new Proxy(init, {
105
- get(_target, p, _receiver) {
106
- return state.get()[p];
107
- },
108
- set(_target, p, newValue, _receiver) {
109
- state.set({
110
- ...state.get(),
111
- [p]: maybeReactiveObjectType(newValue, options)
112
- });
113
- return true;
114
- }
115
- });
116
- return proxy;
117
- };
118
- var $array = (init, options = { deep: false }) => {
119
- if (options.deep) {
120
- for (const [key, value] of Object.entries(init)) {
121
- init[key] = maybeReactiveObjectType(value, options);
122
- }
123
- }
124
- const state = new Signal.State(init);
125
- const proxy = new Proxy(init, {
126
- get(_target, p, _receiver) {
127
- return state.get()[p];
128
- },
129
- set(_target, p, newValue, _receiver) {
130
- state.set({
131
- ...state.get(),
132
- [p]: maybeReactiveObjectType(newValue, options)
133
- });
134
- return true;
135
- }
136
- });
137
- return proxy;
138
- };
139
- var isState = (s) => {
140
- return Signal.isState(s);
141
- };
142
- var isComputed = (s) => {
143
- return Signal.isComputed(s);
144
- };
145
- var getValue = (signal) => {
146
- if (isState(signal) || isComputed(signal)) {
147
- return signal.value;
148
- }
149
- return signal;
150
- };
151
- var $state = (initialValue, options) => {
152
- return new ReactiveValue(initialValue, options);
153
- };
154
- var $computed = (computation, options) => {
155
- return new ReactiveComputation(computation, options);
156
- };
157
- var pending = false;
158
- var watcher = new Signal.subtle.Watcher(() => {
159
- if (!pending) {
160
- pending = true;
161
- queueMicrotask(() => {
162
- pending = false;
163
- for (const s of watcher.getPending()) s.get();
164
- watcher.watch();
165
- });
166
- }
167
- });
168
- var $effect = (cb, options) => {
169
- if (options?.signal?.aborted) return () => {
170
- };
171
- let destroy;
172
- const c = new Signal.Computed(() => {
173
- destroy?.();
174
- destroy = cb() ?? void 0;
175
- });
176
- watcher.watch(c);
177
- c.get();
178
- let cleaned = false;
179
- const cleanup = () => {
180
- if (cleaned) return;
181
- destroy?.();
182
- watcher.unwatch(c);
183
- cleaned = true;
184
- };
185
- options?.signal.addEventListener("abort", cleanup);
186
- return cleanup;
187
- };
188
-
189
- // src/handler-registry.ts
190
- var HandlerRegistry = class extends HTMLElement {
191
- #cleanup = [];
192
- /**
193
- * References the handler's `AbortController`.
194
- *
195
- * The `abort` method of the controller is called in the `disconnectedCallback` method. It allows to cleanup event handlers and other abortable operations
196
- */
197
- abortController;
198
- constructor() {
199
- super();
200
- this.abortController = new AbortController();
201
- }
202
- /**
203
- * Creates an effect that is automatically cleaned up when the component is disconnected
204
- *
205
- * An optional AbortSignal can be provided to abort the effect prematurely
206
- */
207
- $effect(callback, options) {
208
- const signals = [this.abortController.signal];
209
- if (options?.signal) signals.push(options.signal);
210
- $effect(callback, { ...options, signal: AbortSignal.any(signals) });
211
- }
212
- #get(identifier) {
213
- return this[identifier];
214
- }
215
- #handleOn(e) {
216
- if (e instanceof CustomEvent) {
217
- const { handler, type } = e.detail;
218
- if (handler in this && typeof this.#get(handler) === "function") {
219
- e.target?.addEventListener(type, this.#get(handler).bind(this));
220
- e.stopPropagation();
221
- }
222
- }
223
- }
224
- #handleClass(e) {
225
- const target = e.target;
226
- if (e instanceof CustomEvent && target) {
227
- const { identifier } = e.detail;
228
- if (identifier in this) {
229
- this.$effect(() => {
230
- const classList = getValue(this.#get(identifier));
231
- if (classList && typeof classList === "object") {
232
- for (const [k, v] of Object.entries(classList)) {
233
- const force = !!getValue(v);
234
- for (const className of k.split(" ")) {
235
- target.classList.toggle(
236
- className,
237
- force
238
- );
239
- }
240
- }
241
- }
242
- });
243
- e.stopPropagation();
244
- }
245
- }
246
- }
247
- #handleUse(e) {
248
- if (e instanceof CustomEvent) {
249
- const { hook } = e.detail;
250
- if (hook in this && typeof this.#get(hook) === "function") {
251
- const cleanup = this.#get(hook).bind(this)(e.target);
252
- if (typeof cleanup === "function") {
253
- this.#cleanup.push(cleanup);
254
- }
255
- e.stopPropagation();
256
- }
257
- }
258
- }
259
- #handleAttr(e) {
260
- if (e instanceof CustomEvent) {
261
- const { identifier, attribute } = e.detail;
262
- const target = e.target;
263
- if (identifier in this && target instanceof HTMLElement && attribute in target) {
264
- const ref = this.#get(identifier);
265
- const setAttr = () => {
266
- const value = getValue(ref);
267
- if (booleanAttributes.includes(attribute)) {
268
- value ? target.setAttribute(attribute, "") : target.removeAttribute(attribute);
269
- } else {
270
- target.setAttribute(attribute, `${value}`);
271
- }
272
- };
273
- if (isState(ref) || isComputed(ref)) {
274
- this.$effect(() => setAttr());
275
- } else {
276
- setAttr();
277
- }
278
- e.stopPropagation();
279
- }
280
- }
281
- }
282
- #handleProp(e) {
283
- if (e instanceof CustomEvent) {
284
- const { identifier, property } = e.detail;
285
- const target = e.target;
286
- if (identifier in this && target && property in target) {
287
- const ref = this.#get(identifier);
288
- const setProp = () => {
289
- const value = getValue(ref);
290
- target[property] = value;
291
- };
292
- if (isState(ref) || isComputed(ref)) {
293
- this.$effect(() => setProp());
294
- } else {
295
- setProp();
296
- }
297
- e.stopPropagation();
298
- }
299
- }
300
- }
301
- #handleText(e) {
302
- if (e instanceof CustomEvent) {
303
- const target = e.target;
304
- const { identifier } = e.detail;
305
- if (identifier in this && target instanceof HTMLElement) {
306
- const ref = this.#get(identifier);
307
- const setTextContent = () => {
308
- const value = getValue(ref);
309
- target.textContent = `${value}`;
310
- };
311
- if (isState(ref) || isComputed(ref)) {
312
- this.$effect(() => setTextContent());
313
- } else {
314
- setTextContent();
315
- }
316
- e.stopPropagation();
317
- }
318
- }
319
- }
320
- #handleHTML(e) {
321
- if (e instanceof CustomEvent) {
322
- const { identifier } = e.detail;
323
- const target = e.target;
324
- if (identifier in this && target instanceof HTMLElement) {
325
- const ref = this.#get(identifier);
326
- const setInnerHTML = () => {
327
- const value = getValue(ref);
328
- target.innerHTML = `${value}`;
329
- };
330
- if (isState(ref) || isComputed(ref)) {
331
- this.$effect(() => setInnerHTML());
332
- } else {
333
- setInnerHTML();
334
- }
335
- e.stopPropagation();
336
- }
337
- }
338
- }
339
- #handleBind(e) {
340
- if (e instanceof CustomEvent) {
341
- const { identifier, property } = e.detail;
342
- const target = e.target;
343
- if (identifier in this && target instanceof HTMLElement && property in target) {
344
- const state = this.#get(identifier);
345
- if (isState(state)) {
346
- state.value = target[property];
347
- target.addEventListener(bindingConfig[property].event, () => {
348
- state.value = target[property];
349
- });
350
- this.$effect(() => {
351
- target[property] = state.value;
352
- });
353
- }
354
- e.stopPropagation();
355
- }
356
- }
357
- }
358
- connectedCallback() {
359
- const { signal } = this.abortController;
360
- this.addEventListener("@attr-request", this.#handleAttr, { signal });
361
- this.addEventListener("@class-request", this.#handleClass, { signal });
362
- this.addEventListener("@on-request", this.#handleOn, { signal });
363
- this.addEventListener("@use-request", this.#handleUse, { signal });
364
- this.addEventListener("@prop-request", this.#handleProp, { signal });
365
- this.addEventListener("@html-request", this.#handleHTML, { signal });
366
- this.addEventListener("@text-request", this.#handleText, { signal });
367
- this.addEventListener("@bind-request", this.#handleBind, { signal });
368
- }
369
- disconnectedCallback() {
370
- this.abortController.abort();
371
- for (const cleanup of this.#cleanup) {
372
- cleanup();
373
- }
374
- }
375
- };
376
- if (window && !customElements?.get("handler-registry")) {
377
- customElements.define("handler-registry", HandlerRegistry);
378
- }
379
-
380
- export { $array, $computed, $effect, $object, $state, HandlerRegistry, ReactiveComputation, ReactiveValue, getValue, isComputed, isState };
1
+ export { HandlerRegistry } from "./handler-registry.js";
2
+ export * from "./reactivity.js";
3
+ export * from "./handlers.js";
@@ -0,0 +1,162 @@
1
+ import { Signal } from "signal-polyfill";
2
+
3
+ export class ReactiveValue extends Signal.State {
4
+ get;
5
+ set;
6
+
7
+ get value() {
8
+ return super.get();
9
+ }
10
+
11
+ set value(newValue) {
12
+ super.set(newValue);
13
+ }
14
+ }
15
+
16
+ export class ReactiveComputation extends Signal.Computed {
17
+ get;
18
+
19
+ get value() {
20
+ return super.get();
21
+ }
22
+ }
23
+
24
+ const maybeReactiveObjectType = (thing, options) => {
25
+ if (typeof thing === "object") {
26
+ if (Array.isArray(thing)) {
27
+ return $array(thing, options);
28
+ } else if (thing) {
29
+ return $object(thing, options);
30
+ }
31
+ }
32
+ return thing;
33
+ };
34
+
35
+ export const $object = (
36
+ init,
37
+ options = { deep: false },
38
+ ) => {
39
+ if (options.deep === true) {
40
+ for (const [key, value] of Object.entries(init)) {
41
+ init[key] = maybeReactiveObjectType(value, options);
42
+ }
43
+ }
44
+ const state = new Signal.State(init);
45
+
46
+ const proxy = new Proxy(init, {
47
+ get(_target, p, _receiver) {
48
+ return state.get()[p];
49
+ },
50
+ set(_target, p, newValue, _receiver) {
51
+ state.set({
52
+ ...state.get(),
53
+ [p]: maybeReactiveObjectType(newValue, options),
54
+ });
55
+ return true;
56
+ },
57
+ });
58
+
59
+ return proxy;
60
+ };
61
+
62
+ export const $array = (
63
+ init,
64
+ options = { deep: false },
65
+ ) => {
66
+ if (options.deep) {
67
+ for (const [key, value] of Object.entries(init)) {
68
+ init[key] = maybeReactiveObjectType(value, options);
69
+ }
70
+ }
71
+ const state = new Signal.State(init);
72
+
73
+ const proxy = new Proxy(init, {
74
+ get(_target, p, _receiver) {
75
+ return state.get()[p];
76
+ },
77
+ set(_target, p, newValue, _receiver) {
78
+ state.set({
79
+ ...state.get(),
80
+ [p]: maybeReactiveObjectType(newValue, options),
81
+ });
82
+ return true;
83
+ },
84
+ });
85
+
86
+ return proxy;
87
+ };
88
+
89
+ export const isState = (
90
+ s,
91
+ ) => {
92
+ return Signal.isState(s);
93
+ };
94
+
95
+ export const isComputed = (
96
+ s,
97
+ ) => {
98
+ return Signal.isComputed(s);
99
+ };
100
+
101
+ export const getValue = (signal) => {
102
+ if (isState(signal) || isComputed(signal)) {
103
+ return signal.value;
104
+ }
105
+ return signal;
106
+ };
107
+
108
+ export const $state = (
109
+ initialValue,
110
+ options,
111
+ ) => {
112
+ return new ReactiveValue(initialValue, options);
113
+ };
114
+
115
+ export const $computed = (
116
+ computation,
117
+ options,
118
+ ) => {
119
+ return new ReactiveComputation(computation, options);
120
+ };
121
+
122
+ let pending = false;
123
+
124
+ const watcher = new Signal.subtle.Watcher(() => {
125
+ if (!pending) {
126
+ pending = true;
127
+
128
+ queueMicrotask(() => {
129
+ pending = false;
130
+ for (const s of watcher.getPending()) s.get();
131
+ watcher.watch();
132
+ });
133
+ }
134
+ });
135
+
136
+ export const $effect = (
137
+ cb,
138
+ options,
139
+ ) => {
140
+ if (options?.signal?.aborted) return () => {};
141
+
142
+ let destroy;
143
+ const c = new Signal.Computed(() => {
144
+ destroy?.();
145
+ destroy = cb() ?? undefined;
146
+ });
147
+ watcher.watch(c);
148
+ c.get();
149
+
150
+ let cleaned = false;
151
+
152
+ const cleanup = () => {
153
+ if (cleaned) return;
154
+ destroy?.();
155
+ watcher.unwatch(c);
156
+ cleaned = true;
157
+ };
158
+
159
+ options?.signal.addEventListener("abort", cleanup);
160
+
161
+ return cleanup;
162
+ };
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,65 @@
1
+ const is_upper = /[A-Z]/;
2
+ export const spaces_sep_by_comma = /\s*,\s*/;
3
+
4
+ export const toKebabCase = (str) => {
5
+ let kebab = "";
6
+ for (let index = 0; index < str.length; index++) {
7
+ const char = str[index];
8
+
9
+ if (index !== 0 && is_upper.test(char)) {
10
+ kebab += `-${char.toLowerCase()}`;
11
+ } else {
12
+ kebab += char.toLowerCase();
13
+ }
14
+ }
15
+ return kebab;
16
+ };
17
+
18
+ export const toPascalCase = (str) => {
19
+ let pascal = "";
20
+ let toUpper = true;
21
+
22
+ for (let index = 0; index < str.length; index++) {
23
+ const char = str[index];
24
+
25
+ if (char === "-") {
26
+ toUpper = true;
27
+ continue;
28
+ }
29
+
30
+ if (toUpper) {
31
+ pascal += char.toUpperCase();
32
+ toUpper = false;
33
+ } else {
34
+ pascal += char.toLowerCase();
35
+ }
36
+ }
37
+ return pascal;
38
+ };
39
+
40
+ export const booleanAttributes = [
41
+ "allowfullscreen", // on <iframe>
42
+ "async", // on <script>
43
+ "autofocus", // on <button>, <input>, <select>, <textarea>
44
+ "autoplay", // on <audio>, <video>
45
+ "checked", // on <input type="checkbox">, <input type="radio">
46
+ "controls", // on <audio>, <video>
47
+ "default", // on <track>
48
+ "defer", // on <script>
49
+ "disabled", // on form elements like <button>, <fieldset>, <input>, <optgroup>, <option>,<select>, <textarea>
50
+ "formnovalidate", // on <button>, <input type="submit">
51
+ "hidden", // global
52
+ "inert", // global
53
+ "ismap", // on <img>
54
+ "itemscope", // global; part of microdata
55
+ "loop", // on <audio>, <video>
56
+ "multiple", // on <input type="file">, <select>
57
+ "muted", // on <audio>, <video>
58
+ "nomodule", // on <script>
59
+ "novalidate", // on <form>
60
+ "open", // on <details>
61
+ "readonly", // on <input>, <textarea>
62
+ "required", // on <input>, <select>, <textarea>
63
+ "reversed", // on <ol>
64
+ "selected", // on <option>
65
+ ];
package/deno.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "tasks": {
3
+ "check": "deno fmt --check && deno lint && deno check **/*.ts",
4
+ "build": "deno run -A build.ts"
5
+ },
6
+ "fmt": {
7
+ "exclude": ["**/*.md"]
8
+ },
9
+ "compilerOptions": {
10
+ "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"],
11
+ "checkJs": true,
12
+ "noImplicitOverride": true
13
+ },
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./client": "./client/index.js"
17
+ },
18
+ "imports": {
19
+ "signal-polyfill": "npm:signal-polyfill@^0.2.2"
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,31 +1,12 @@
1
1
  {
2
2
  "name": "@radishland/runtime",
3
- "version": "0.0.7",
3
+ "version": "1.0.0-alpha.1",
4
4
  "type": "module",
5
5
  "description": "The Radish runtime",
6
+ "main": "./client/index.js",
6
7
  "author": "Frédéric Crozatier",
7
8
  "license": "MIT",
8
- "scripts": {
9
- "build": "tsup",
10
- "prepublishOnly": "pnpm build"
11
- },
12
- "exports": {
13
- ".": {
14
- "import": "./client/index.js",
15
- "types": "./client/index.d.ts"
16
- }
17
- },
18
- "files": [
19
- "./client",
20
- "README.md",
21
- "LICENCE",
22
- "package.json"
23
- ],
24
9
  "dependencies": {
25
10
  "signal-polyfill": "^0.2.2"
26
- },
27
- "devDependencies": {
28
- "tsup": "^8.4.0",
29
- "typescript": "^5.7.3"
30
11
  }
31
12
  }
package/src/config.ts ADDED
@@ -0,0 +1,12 @@
1
+ export const bindingConfig = {
2
+ "checked": {
3
+ element: ["input"],
4
+ type: ["boolean"],
5
+ event: "change",
6
+ },
7
+ "value": {
8
+ element: ["input", "select", "textarea"],
9
+ type: ["string", "number"],
10
+ event: "input",
11
+ },
12
+ };