@erickxavier/no-js 1.0.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.
@@ -0,0 +1,144 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVES: ref, use, call
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import {
6
+ _refs,
7
+ _stores,
8
+ _notifyStoreWatchers,
9
+ } from "../globals.js";
10
+ import { createContext } from "../context.js";
11
+ import { evaluate, _execStatement, _interpolate } from "../evaluate.js";
12
+ import { _doFetch } from "../fetch.js";
13
+ import { findContext, _cloneTemplate } from "../dom.js";
14
+ import { registerDirective, processTree } from "../registry.js";
15
+
16
+ registerDirective("ref", {
17
+ priority: 5,
18
+ init(el, name, refName) {
19
+ _refs[refName] = el;
20
+ },
21
+ });
22
+
23
+ registerDirective("use", {
24
+ priority: 10,
25
+ init(el, name, tplId) {
26
+ const ctx = findContext(el);
27
+ const clone = _cloneTemplate(tplId);
28
+ if (!clone) return;
29
+
30
+ // Collect var-* attributes
31
+ const vars = {};
32
+ for (const attr of [...el.attributes]) {
33
+ if (attr.name.startsWith("var-")) {
34
+ const varName = attr.name.replace("var-", "");
35
+ vars[varName] = evaluate(attr.value, ctx);
36
+ }
37
+ }
38
+
39
+ const childCtx = createContext(vars, ctx);
40
+
41
+ // Handle slots
42
+ const slots = {};
43
+ for (const child of [...el.children]) {
44
+ const slotName = child.getAttribute("slot") || "default";
45
+ if (!slots[slotName])
46
+ slots[slotName] = document.createDocumentFragment();
47
+ slots[slotName].appendChild(child.cloneNode(true));
48
+ }
49
+
50
+ // Replace <slot> elements in template
51
+ const slotEls = clone.querySelectorAll("slot");
52
+ for (const slotEl of slotEls) {
53
+ const slotName = slotEl.getAttribute("name") || "default";
54
+ if (slots[slotName]) {
55
+ slotEl.replaceWith(slots[slotName]);
56
+ }
57
+ }
58
+
59
+ el.innerHTML = "";
60
+ const wrapper = document.createElement("div");
61
+ wrapper.style.display = "contents";
62
+ wrapper.__ctx = childCtx;
63
+ wrapper.appendChild(clone);
64
+ el.appendChild(wrapper);
65
+ processTree(wrapper);
66
+ },
67
+ });
68
+
69
+ registerDirective("call", {
70
+ priority: 20,
71
+ init(el, name, url) {
72
+ const ctx = findContext(el);
73
+ const method = el.getAttribute("method") || "get";
74
+ const asKey = el.getAttribute("as");
75
+ const intoStore = el.getAttribute("into");
76
+ const successTpl = el.getAttribute("success");
77
+ const errorTpl = el.getAttribute("error");
78
+ const thenExpr = el.getAttribute("then");
79
+ const confirmMsg = el.getAttribute("confirm");
80
+ const bodyAttr = el.getAttribute("body");
81
+
82
+ el.addEventListener("click", async (e) => {
83
+ e.preventDefault();
84
+ if (confirmMsg && !window.confirm(confirmMsg)) return;
85
+
86
+ const resolvedUrl = _interpolate(url, ctx);
87
+ try {
88
+ let reqBody = null;
89
+ if (bodyAttr) {
90
+ const interpolated = _interpolate(bodyAttr, ctx);
91
+ try {
92
+ reqBody = JSON.parse(interpolated);
93
+ } catch {
94
+ reqBody = interpolated;
95
+ }
96
+ }
97
+ const data = await _doFetch(resolvedUrl, method, reqBody, {}, el);
98
+ if (asKey) ctx.$set(asKey, data);
99
+ if (asKey && intoStore) {
100
+ if (!_stores[intoStore]) _stores[intoStore] = createContext({});
101
+ _stores[intoStore].$set(asKey, data);
102
+ _notifyStoreWatchers();
103
+ }
104
+ if (thenExpr) _execStatement(thenExpr, ctx, { result: data });
105
+ if (successTpl) {
106
+ const clone = _cloneTemplate(successTpl);
107
+ if (clone) {
108
+ const tplEl = document.getElementById(
109
+ successTpl.replace("#", ""),
110
+ );
111
+ const vn = tplEl?.getAttribute("var") || "result";
112
+ const childCtx = createContext({ [vn]: data }, ctx);
113
+ const target = el.closest("[route-view]") || el.parentElement;
114
+ const wrapper = document.createElement("div");
115
+ wrapper.style.display = "contents";
116
+ wrapper.__ctx = childCtx;
117
+ wrapper.appendChild(clone);
118
+ target.appendChild(wrapper);
119
+ processTree(wrapper);
120
+ }
121
+ }
122
+ } catch (err) {
123
+ if (errorTpl) {
124
+ const clone = _cloneTemplate(errorTpl);
125
+ if (clone) {
126
+ const tplEl = document.getElementById(errorTpl.replace("#", ""));
127
+ const vn = tplEl?.getAttribute("var") || "err";
128
+ const childCtx = createContext(
129
+ { [vn]: { message: err.message, status: err.status } },
130
+ ctx,
131
+ );
132
+ const target = el.parentElement;
133
+ const wrapper = document.createElement("div");
134
+ wrapper.style.display = "contents";
135
+ wrapper.__ctx = childCtx;
136
+ wrapper.appendChild(clone);
137
+ target.appendChild(wrapper);
138
+ processTree(wrapper);
139
+ }
140
+ }
141
+ }
142
+ });
143
+ },
144
+ });
@@ -0,0 +1,102 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVES: state, store, computed, watch
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { _stores, _log } from "../globals.js";
6
+ import { createContext } from "../context.js";
7
+ import { evaluate, _execStatement } from "../evaluate.js";
8
+ import { findContext } from "../dom.js";
9
+ import { registerDirective } from "../registry.js";
10
+
11
+ registerDirective("state", {
12
+ priority: 0,
13
+ init(el, name, value) {
14
+ const data = evaluate(value, createContext()) || {};
15
+ const parent = el.parentElement ? findContext(el.parentElement) : null;
16
+ const ctx = createContext(data, parent);
17
+ el.__ctx = ctx;
18
+
19
+ // Persistence
20
+ const persist = el.getAttribute("persist");
21
+ const persistKey = el.getAttribute("persist-key");
22
+ if (persist && persistKey) {
23
+ const store =
24
+ persist === "localStorage"
25
+ ? localStorage
26
+ : persist === "sessionStorage"
27
+ ? sessionStorage
28
+ : null;
29
+ if (store) {
30
+ try {
31
+ const saved = store.getItem("nojs_state_" + persistKey);
32
+ if (saved) {
33
+ const parsed = JSON.parse(saved);
34
+ for (const [k, v] of Object.entries(parsed)) ctx.$set(k, v);
35
+ }
36
+ } catch {
37
+ /* ignore */
38
+ }
39
+ ctx.$watch(() => {
40
+ try {
41
+ store.setItem(
42
+ "nojs_state_" + persistKey,
43
+ JSON.stringify(ctx.__raw),
44
+ );
45
+ } catch {
46
+ /* ignore */
47
+ }
48
+ });
49
+ }
50
+ }
51
+
52
+ _log("state", data);
53
+ },
54
+ });
55
+
56
+ registerDirective("store", {
57
+ priority: 0,
58
+ init(el, name, storeName) {
59
+ const valueAttr = el.getAttribute("value");
60
+ if (!storeName) return;
61
+ if (!_stores[storeName]) {
62
+ const data = valueAttr
63
+ ? evaluate(valueAttr, createContext()) || {}
64
+ : {};
65
+ _stores[storeName] = createContext(data);
66
+ }
67
+ _log("store", storeName);
68
+ },
69
+ });
70
+
71
+ registerDirective("computed", {
72
+ priority: 2,
73
+ init(el, name, computedName) {
74
+ const expr = el.getAttribute("expr");
75
+ if (!computedName || !expr) return;
76
+ const ctx = findContext(el);
77
+ function update() {
78
+ const val = evaluate(expr, ctx);
79
+ ctx.$set(computedName, val);
80
+ }
81
+ ctx.$watch(update);
82
+ update();
83
+ },
84
+ });
85
+
86
+ registerDirective("watch", {
87
+ priority: 2,
88
+ init(el, name, watchExpr) {
89
+ const ctx = findContext(el);
90
+ const onChange = el.getAttribute("on:change");
91
+ let lastVal = evaluate(watchExpr, ctx);
92
+ ctx.$watch(() => {
93
+ const newVal = evaluate(watchExpr, ctx);
94
+ if (newVal !== lastVal) {
95
+ const oldVal = lastVal;
96
+ lastVal = newVal;
97
+ if (onChange)
98
+ _execStatement(onChange, ctx, { $old: oldVal, $new: newVal });
99
+ }
100
+ });
101
+ },
102
+ });
@@ -0,0 +1,88 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVES: class-*, style-*
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { evaluate } from "../evaluate.js";
6
+ import { findContext } from "../dom.js";
7
+ import { registerDirective } from "../registry.js";
8
+
9
+ registerDirective("class-*", {
10
+ priority: 20,
11
+ init(el, name, expr) {
12
+ const suffix = name.replace("class-", "");
13
+ const ctx = findContext(el);
14
+
15
+ // class-map="{ active: x, bold: y }"
16
+ if (suffix === "map") {
17
+ function update() {
18
+ const obj = evaluate(expr, ctx);
19
+ if (obj && typeof obj === "object") {
20
+ for (const [cls, cond] of Object.entries(obj)) {
21
+ el.classList.toggle(cls, !!cond);
22
+ }
23
+ }
24
+ }
25
+ ctx.$watch(update);
26
+ update();
27
+ return;
28
+ }
29
+
30
+ // class-list="['a', condition ? 'b' : 'c']"
31
+ if (suffix === "list") {
32
+ let prevClasses = [];
33
+ function update() {
34
+ const arr = evaluate(expr, ctx);
35
+ if (Array.isArray(arr)) {
36
+ prevClasses.forEach((cls) => {
37
+ if (cls) el.classList.remove(cls);
38
+ });
39
+ const next = arr.filter(Boolean);
40
+ next.forEach((cls) => el.classList.add(cls));
41
+ prevClasses = next;
42
+ }
43
+ }
44
+ ctx.$watch(update);
45
+ update();
46
+ return;
47
+ }
48
+
49
+ // class-{name}="expr"
50
+ function update() {
51
+ el.classList.toggle(suffix, !!evaluate(expr, ctx));
52
+ }
53
+ ctx.$watch(update);
54
+ update();
55
+ },
56
+ });
57
+
58
+ registerDirective("style-*", {
59
+ priority: 20,
60
+ init(el, name, expr) {
61
+ const suffix = name.replace("style-", "");
62
+ const ctx = findContext(el);
63
+
64
+ // style-map="{ color: x, fontSize: y }"
65
+ if (suffix === "map") {
66
+ function update() {
67
+ const obj = evaluate(expr, ctx);
68
+ if (obj && typeof obj === "object") {
69
+ for (const [prop, val] of Object.entries(obj)) {
70
+ el.style[prop] = val ?? "";
71
+ }
72
+ }
73
+ }
74
+ ctx.$watch(update);
75
+ update();
76
+ return;
77
+ }
78
+
79
+ // style-{property}="expr" (e.g. style-color, style-font-size)
80
+ const cssProp = suffix.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
81
+ function update() {
82
+ const val = evaluate(expr, ctx);
83
+ el.style[cssProp] = val != null ? String(val) : "";
84
+ }
85
+ ctx.$watch(update);
86
+ update();
87
+ },
88
+ });
@@ -0,0 +1,216 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVES: validate, error-boundary
3
+ // HELPER: _validateField
4
+ // ═══════════════════════════════════════════════════════════════════════
5
+
6
+ import { _validators } from "../globals.js";
7
+ import { createContext } from "../context.js";
8
+ import { findContext, _cloneTemplate } from "../dom.js";
9
+ import { registerDirective, processTree } from "../registry.js";
10
+
11
+ function _validateField(value, rules, allValues) {
12
+ const ruleList = rules.split("|").map((r) => r.trim());
13
+ for (const rule of ruleList) {
14
+ const [name, ...args] = rule.split(":");
15
+ const fn = _validators[name];
16
+ if (fn) {
17
+ const result = fn(value, ...args, allValues);
18
+ if (result !== true && result) return result;
19
+ } else {
20
+ // Built-in validators
21
+ switch (name) {
22
+ case "email":
23
+ if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
24
+ return "Invalid email";
25
+ break;
26
+ case "url":
27
+ try {
28
+ new URL(value);
29
+ } catch {
30
+ return "Invalid URL";
31
+ }
32
+ break;
33
+ case "min":
34
+ if (Number(value) < Number(args[0]))
35
+ return `Minimum value is ${args[0]}`;
36
+ break;
37
+ case "max":
38
+ if (Number(value) > Number(args[0]))
39
+ return `Maximum value is ${args[0]}`;
40
+ break;
41
+ case "between": {
42
+ const n = Number(value);
43
+ if (n < Number(args[0]) || n > Number(args[1]))
44
+ return `Must be between ${args[0]} and ${args[1]}`;
45
+ break;
46
+ }
47
+ case "match":
48
+ if (value !== allValues[args[0]]) return `Must match ${args[0]}`;
49
+ break;
50
+ case "phone":
51
+ if (!/^[\d\s\-+()]{7,15}$/.test(value))
52
+ return "Invalid phone number";
53
+ break;
54
+ case "cpf":
55
+ if (!/^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$/.test(value))
56
+ return "Invalid CPF";
57
+ break;
58
+ case "cnpj":
59
+ if (!/^\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2}$/.test(value))
60
+ return "Invalid CNPJ";
61
+ break;
62
+ case "creditcard":
63
+ if (!/^\d{13,19}$/.test(value.replace(/\s|-/g, "")))
64
+ return "Invalid card number";
65
+ break;
66
+ case "custom":
67
+ if (args[0] && _validators[args[0]]) {
68
+ const result = _validators[args[0]](value, allValues);
69
+ if (result !== true && result) return result;
70
+ }
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ registerDirective("validate", {
79
+ priority: 30,
80
+ init(el, name, rules) {
81
+ const ctx = findContext(el);
82
+
83
+ // If on a form, set up form-level validation
84
+ if (el.tagName === "FORM") {
85
+ const formCtx = {
86
+ valid: false,
87
+ dirty: false,
88
+ touched: false,
89
+ submitting: false,
90
+ errors: {},
91
+ values: {},
92
+ reset: () => {
93
+ el.reset();
94
+ checkValidity();
95
+ },
96
+ };
97
+ ctx.$set("$form", formCtx);
98
+
99
+ function checkValidity() {
100
+ const errors = {};
101
+ const values = {};
102
+ let valid = true;
103
+
104
+ for (const field of el.querySelectorAll("input, textarea, select")) {
105
+ if (!field.name) continue;
106
+ values[field.name] = field.value;
107
+
108
+ const fieldRules = field.getAttribute("validate");
109
+ if (fieldRules) {
110
+ const err = _validateField(field.value, fieldRules, values);
111
+ if (err) {
112
+ errors[field.name] = err;
113
+ valid = false;
114
+ }
115
+ }
116
+
117
+ if (!field.checkValidity()) {
118
+ errors[field.name] =
119
+ errors[field.name] || field.validationMessage;
120
+ valid = false;
121
+ }
122
+ }
123
+
124
+ formCtx.valid = valid;
125
+ formCtx.errors = errors;
126
+ formCtx.values = values;
127
+ ctx.$set("$form", { ...formCtx });
128
+ }
129
+
130
+ el.addEventListener("input", () => {
131
+ formCtx.dirty = true;
132
+ checkValidity();
133
+ });
134
+ el.addEventListener("focusout", () => {
135
+ formCtx.touched = true;
136
+ checkValidity();
137
+ });
138
+ el.addEventListener("submit", () => {
139
+ formCtx.submitting = true;
140
+ ctx.$set("$form", { ...formCtx });
141
+ requestAnimationFrame(() => {
142
+ formCtx.submitting = false;
143
+ ctx.$set("$form", { ...formCtx });
144
+ });
145
+ });
146
+
147
+ // Initial check
148
+ requestAnimationFrame(checkValidity);
149
+ return;
150
+ }
151
+
152
+ // Field-level validation
153
+ if (
154
+ rules &&
155
+ (el.tagName === "INPUT" ||
156
+ el.tagName === "TEXTAREA" ||
157
+ el.tagName === "SELECT")
158
+ ) {
159
+ const errorTpl = el.getAttribute("error");
160
+ el.addEventListener("input", () => {
161
+ const err = _validateField(el.value, rules, {});
162
+ if (err && errorTpl) {
163
+ // Show error
164
+ let errorEl = el.nextElementSibling?.__validationError
165
+ ? el.nextElementSibling
166
+ : null;
167
+ if (!errorEl) {
168
+ errorEl = document.createElement("div");
169
+ errorEl.__validationError = true;
170
+ errorEl.style.display = "contents";
171
+ el.parentNode.insertBefore(errorEl, el.nextSibling);
172
+ }
173
+ const clone = _cloneTemplate(errorTpl);
174
+ if (clone) {
175
+ const childCtx = createContext({ err: { message: err } }, ctx);
176
+ errorEl.innerHTML = "";
177
+ errorEl.__ctx = childCtx;
178
+ errorEl.appendChild(clone);
179
+ processTree(errorEl);
180
+ }
181
+ } else {
182
+ const errorEl = el.nextElementSibling?.__validationError
183
+ ? el.nextElementSibling
184
+ : null;
185
+ if (errorEl) errorEl.innerHTML = "";
186
+ }
187
+ });
188
+ }
189
+ },
190
+ });
191
+
192
+ registerDirective("error-boundary", {
193
+ priority: 1,
194
+ init(el, name, fallbackTpl) {
195
+ const ctx = findContext(el);
196
+
197
+ window.addEventListener("error", (e) => {
198
+ if (el.contains(e.target) || el === e.target) {
199
+ const clone = _cloneTemplate(fallbackTpl);
200
+ if (clone) {
201
+ const childCtx = createContext(
202
+ { err: { message: e.message } },
203
+ ctx,
204
+ );
205
+ el.innerHTML = "";
206
+ const wrapper = document.createElement("div");
207
+ wrapper.style.display = "contents";
208
+ wrapper.__ctx = childCtx;
209
+ wrapper.appendChild(clone);
210
+ el.appendChild(wrapper);
211
+ processTree(wrapper);
212
+ }
213
+ }
214
+ });
215
+ },
216
+ });