@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 ADDED
@@ -0,0 +1,14 @@
1
+ import { emptyDirSync, walkSync } from "@std/fs";
2
+ import stripTypes from "@fcrozatier/type-strip";
3
+ import { join } from "@std/path";
4
+
5
+ console.log("Building client runtime...");
6
+ emptyDirSync("client");
7
+
8
+ for (const entry of walkSync("src", { exts: [".ts"] })) {
9
+ const content = Deno.readTextFileSync(entry.path);
10
+ const dest = join("client", entry.name.replace(/\.ts$/, ".js"));
11
+ const js = stripTypes(content, { pathRewriting: true, removeComments: true });
12
+ Deno.writeTextFileSync(dest, js);
13
+ }
14
+ console.log("Done!");
@@ -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
+ };
@@ -0,0 +1,236 @@
1
+ import { booleanAttributes } from "./utils.js";
2
+ import { bindingConfig } from "./config.js";
3
+
4
+ import { $effect, getValue, isComputed, isState } from "./reactivity.js";
5
+
6
+ export class HandlerRegistry extends HTMLElement {
7
+
8
+ #cleanup = [];
9
+
10
+ abortController;
11
+
12
+ constructor() {
13
+ super();
14
+ this.abortController = new AbortController();
15
+ }
16
+
17
+ $effect(callback, options) {
18
+ const signals = [this.abortController.signal];
19
+ if (options?.signal) signals.push(options.signal);
20
+
21
+ $effect(callback, { ...options, signal: AbortSignal.any(signals) });
22
+ }
23
+
24
+ #get(identifier) {
25
+ return this[identifier];
26
+ }
27
+
28
+ #handleOn(e) {
29
+ if (e instanceof CustomEvent) {
30
+ const { handler, type } = e.detail;
31
+
32
+ if (handler in this && typeof this.#get(handler) === "function") {
33
+ e.target?.addEventListener(type, this.#get(handler).bind(this));
34
+ e.stopPropagation();
35
+ }
36
+ }
37
+ }
38
+
39
+ #handleClass(e) {
40
+ const target = e.target;
41
+ if (e instanceof CustomEvent && target) {
42
+ const { identifier } = e.detail;
43
+
44
+ if (identifier in this) {
45
+ this.$effect(() => {
46
+ const classList = getValue(this.#get(identifier));
47
+ if (classList && typeof classList === "object") {
48
+ for (const [k, v] of Object.entries(classList)) {
49
+ const force = !!getValue(v);
50
+ for (const className of k.split(" ")) {
51
+ target.classList.toggle(
52
+ className,
53
+ force,
54
+ );
55
+ }
56
+ }
57
+ }
58
+ });
59
+
60
+ e.stopPropagation();
61
+ }
62
+ }
63
+ }
64
+
65
+ #handleUse(e) {
66
+ if (e instanceof CustomEvent) {
67
+ const { hook } = e.detail;
68
+
69
+ if (hook in this && typeof this.#get(hook) === "function") {
70
+ const cleanup = this.#get(hook).bind(this)(e.target);
71
+ if (typeof cleanup === "function") {
72
+ this.#cleanup.push(cleanup);
73
+ }
74
+ e.stopPropagation();
75
+ }
76
+ }
77
+ }
78
+
79
+ #handleAttr(e) {
80
+ if (e instanceof CustomEvent) {
81
+ const { identifier, attribute } = e.detail;
82
+ const target = e.target;
83
+
84
+ if (
85
+ identifier in this && target instanceof HTMLElement &&
86
+ attribute in target
87
+ ) {
88
+ const ref = this.#get(identifier);
89
+
90
+ const setAttr = () => {
91
+ const value = getValue(ref);
92
+ if (booleanAttributes.includes(attribute)) {
93
+ value
94
+ ? target.setAttribute(attribute, "")
95
+ : target.removeAttribute(attribute);
96
+ } else {
97
+ target.setAttribute(attribute, `${value}`);
98
+ }
99
+ };
100
+
101
+ if (isState(ref) || isComputed(ref)) {
102
+ this.$effect(() => setAttr());
103
+ } else {
104
+ setAttr();
105
+ }
106
+
107
+ e.stopPropagation();
108
+ }
109
+ }
110
+ }
111
+
112
+ #handleProp(e) {
113
+ if (e instanceof CustomEvent) {
114
+ const { identifier, property } = e.detail;
115
+ const target = e.target;
116
+
117
+ if (identifier in this && target && property in target) {
118
+ const ref = this.#get(identifier);
119
+
120
+ const setProp = () => {
121
+ const value = getValue(ref);
122
+ target[property] = value;
123
+ };
124
+
125
+ if (isState(ref) || isComputed(ref)) {
126
+ this.$effect(() => setProp());
127
+ } else {
128
+ setProp();
129
+ }
130
+
131
+ e.stopPropagation();
132
+ }
133
+ }
134
+ }
135
+
136
+ #handleText(e) {
137
+ if (e instanceof CustomEvent) {
138
+ const target = e.target;
139
+
140
+ const { identifier } = e.detail;
141
+
142
+ if (identifier in this && target instanceof HTMLElement) {
143
+ const ref = this.#get(identifier);
144
+
145
+ const setTextContent = () => {
146
+ const value = getValue(ref);
147
+ target.textContent = `${value}`;
148
+ };
149
+
150
+ if (isState(ref) || isComputed(ref)) {
151
+ this.$effect(() => setTextContent());
152
+ } else {
153
+ setTextContent();
154
+ }
155
+
156
+ e.stopPropagation();
157
+ }
158
+ }
159
+ }
160
+
161
+ #handleHTML(e) {
162
+ if (e instanceof CustomEvent) {
163
+ const { identifier } = e.detail;
164
+ const target = e.target;
165
+
166
+ if (identifier in this && target instanceof HTMLElement) {
167
+ const ref = this.#get(identifier);
168
+
169
+ const setInnerHTML = () => {
170
+ const value = getValue(ref);
171
+ target.innerHTML = `${value}`;
172
+ };
173
+
174
+ if (isState(ref) || isComputed(ref)) {
175
+ this.$effect(() => setInnerHTML());
176
+ } else {
177
+ setInnerHTML();
178
+ }
179
+
180
+ e.stopPropagation();
181
+ }
182
+ }
183
+ }
184
+
185
+ #handleBind(e) {
186
+ if (e instanceof CustomEvent) {
187
+ const { identifier, property } = e.detail;
188
+ const target = e.target;
189
+
190
+ if (
191
+ identifier in this && target instanceof HTMLElement &&
192
+ property in target
193
+ ) {
194
+ const state = this.#get(identifier);
195
+ if (isState(state)) {
196
+ state.value = target[property];
197
+
198
+ target.addEventListener(bindingConfig[property].event, () => {
199
+ state.value = target[property];
200
+ });
201
+
202
+ this.$effect(() => {
203
+ target[property] = state.value;
204
+ });
205
+ }
206
+
207
+ e.stopPropagation();
208
+ }
209
+ }
210
+ }
211
+
212
+ connectedCallback() {
213
+ const { signal } = this.abortController;
214
+
215
+ this.addEventListener("@attr-request", this.#handleAttr, { signal });
216
+ this.addEventListener("@class-request", this.#handleClass, { signal });
217
+ this.addEventListener("@on-request", this.#handleOn, { signal });
218
+ this.addEventListener("@use-request", this.#handleUse, { signal });
219
+ this.addEventListener("@prop-request", this.#handleProp, { signal });
220
+ this.addEventListener("@html-request", this.#handleHTML, { signal });
221
+ this.addEventListener("@text-request", this.#handleText, { signal });
222
+ this.addEventListener("@bind-request", this.#handleBind, { signal });
223
+ }
224
+
225
+ disconnectedCallback() {
226
+ this.abortController.abort();
227
+
228
+ for (const cleanup of this.#cleanup) {
229
+ cleanup();
230
+ }
231
+ }
232
+ }
233
+
234
+ if (window && !customElements.get("handler-registry")) {
235
+ customElements.define("handler-registry", HandlerRegistry);
236
+ }
@@ -0,0 +1,174 @@
1
+ import { bindingConfig } from "./config.js";
2
+ import { spaces_sep_by_comma } from "./utils.js";
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);