@erickxavier/no-js 1.0.1 → 1.0.2
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/dist/cjs/no.js +4 -4
- package/dist/cjs/no.js.map +3 -3
- package/dist/esm/no.js +4 -4
- package/dist/esm/no.js.map +3 -3
- package/dist/iife/no.js +4 -4
- package/dist/iife/no.js.map +3 -3
- package/package.json +9 -2
- package/src/directives/binding.js +1 -1
- package/src/directives/conditionals.js +5 -0
- package/src/directives/i18n.js +2 -1
- package/src/directives/loops.js +14 -10
- package/src/directives/validation.js +30 -14
- package/src/evaluate.js +13 -1
- package/src/i18n.js +17 -1
- package/src/index.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erickxavier/no-js",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "The HTML-first reactive framework — build dynamic web apps with just HTML attributes, no JavaScript required",
|
|
5
5
|
"main": "dist/cjs/no.js",
|
|
6
6
|
"module": "dist/esm/no.js",
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"test:watch": "jest --watch",
|
|
28
28
|
"test:coverage": "jest --coverage",
|
|
29
29
|
"test:verbose": "jest --verbose",
|
|
30
|
+
"test:e2e": "npx playwright test --config e2e/playwright.config.ts",
|
|
31
|
+
"test:e2e:ui": "npx playwright test --config e2e/playwright.config.ts --ui",
|
|
32
|
+
"test:e2e:headed": "npx playwright test --config e2e/playwright.config.ts --headed",
|
|
33
|
+
"test:e2e:report": "npx playwright show-report e2e/playwright-report",
|
|
34
|
+
"test:all": "npm test && npm run test:e2e",
|
|
30
35
|
"prepublishOnly": "npm run build"
|
|
31
36
|
},
|
|
32
37
|
"keywords": [
|
|
@@ -54,11 +59,13 @@
|
|
|
54
59
|
"devDependencies": {
|
|
55
60
|
"@babel/core": "^7.29.0",
|
|
56
61
|
"@babel/preset-env": "^7.29.0",
|
|
62
|
+
"@playwright/test": "^1.58.2",
|
|
57
63
|
"@testing-library/jest-dom": "^6.9.1",
|
|
58
64
|
"babel-jest": "^30.2.0",
|
|
59
65
|
"esbuild": "^0.27.3",
|
|
60
66
|
"jest": "^30.2.0",
|
|
61
67
|
"jest-environment-jsdom": "^30.2.0",
|
|
62
|
-
"jsdom": "^28.1.0"
|
|
68
|
+
"jsdom": "^28.1.0",
|
|
69
|
+
"serve": "^14.2.5"
|
|
63
70
|
}
|
|
64
71
|
}
|
|
@@ -13,7 +13,7 @@ registerDirective("bind", {
|
|
|
13
13
|
const ctx = findContext(el);
|
|
14
14
|
function update() {
|
|
15
15
|
const val = evaluate(expr, ctx);
|
|
16
|
-
|
|
16
|
+
el.textContent = (val !== undefined && val !== null) ? String(val) : '';
|
|
17
17
|
}
|
|
18
18
|
_watchExpr(expr, ctx, update);
|
|
19
19
|
update();
|
|
@@ -82,6 +82,7 @@ registerDirective("else-if", {
|
|
|
82
82
|
// Works like `if` but checks previous sibling's condition
|
|
83
83
|
const ctx = findContext(el);
|
|
84
84
|
const thenId = el.getAttribute("then");
|
|
85
|
+
const originalChildren = [...el.childNodes].map((n) => n.cloneNode(true));
|
|
85
86
|
|
|
86
87
|
function update() {
|
|
87
88
|
// Check if any preceding if/else-if was true
|
|
@@ -108,6 +109,10 @@ registerDirective("else-if", {
|
|
|
108
109
|
el.innerHTML = "";
|
|
109
110
|
el.appendChild(clone);
|
|
110
111
|
}
|
|
112
|
+
} else {
|
|
113
|
+
el.innerHTML = "";
|
|
114
|
+
for (const child of originalChildren)
|
|
115
|
+
el.appendChild(child.cloneNode(true));
|
|
111
116
|
}
|
|
112
117
|
_clearDeclared(el);
|
|
113
118
|
processTree(el);
|
package/src/directives/i18n.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// DIRECTIVE: t (i18n translations)
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════
|
|
4
4
|
|
|
5
|
-
import { _i18n } from "../i18n.js";
|
|
5
|
+
import { _i18n, _watchI18n } from "../i18n.js";
|
|
6
6
|
import { evaluate } from "../evaluate.js";
|
|
7
7
|
import { findContext } from "../dom.js";
|
|
8
8
|
import { registerDirective } from "../registry.js";
|
|
@@ -24,6 +24,7 @@ registerDirective("t", {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
ctx.$watch(update);
|
|
27
|
+
_watchI18n(update);
|
|
27
28
|
update();
|
|
28
29
|
},
|
|
29
30
|
});
|
package/src/directives/loops.js
CHANGED
|
@@ -86,13 +86,15 @@ registerDirective("each", {
|
|
|
86
86
|
el.appendChild(wrapper);
|
|
87
87
|
processTree(wrapper);
|
|
88
88
|
|
|
89
|
-
// Stagger animation
|
|
90
|
-
if (animEnter && stagger) {
|
|
91
|
-
wrapper.style.animationDelay = i * stagger + "ms";
|
|
92
|
-
}
|
|
93
89
|
if (animEnter) {
|
|
94
90
|
const firstChild = wrapper.firstElementChild;
|
|
95
|
-
if (firstChild)
|
|
91
|
+
if (firstChild) {
|
|
92
|
+
firstChild.classList.add(animEnter);
|
|
93
|
+
// Stagger animation — delay must be on the child, not the wrapper
|
|
94
|
+
if (stagger) {
|
|
95
|
+
firstChild.style.animationDelay = i * stagger + "ms";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
96
98
|
}
|
|
97
99
|
});
|
|
98
100
|
}
|
|
@@ -198,13 +200,15 @@ registerDirective("foreach", {
|
|
|
198
200
|
el.appendChild(wrapper);
|
|
199
201
|
processTree(wrapper);
|
|
200
202
|
|
|
201
|
-
// Stagger animation
|
|
202
|
-
if (animEnter && stagger) {
|
|
203
|
-
wrapper.style.animationDelay = (i * stagger) + "ms";
|
|
204
|
-
}
|
|
205
203
|
if (animEnter) {
|
|
206
204
|
const firstChild = wrapper.firstElementChild;
|
|
207
|
-
if (firstChild)
|
|
205
|
+
if (firstChild) {
|
|
206
|
+
firstChild.classList.add(animEnter);
|
|
207
|
+
// Stagger animation — delay must be on the child, not the wrapper
|
|
208
|
+
if (stagger) {
|
|
209
|
+
firstChild.style.animationDelay = (i * stagger) + "ms";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
208
212
|
}
|
|
209
213
|
});
|
|
210
214
|
}
|
|
@@ -19,6 +19,10 @@ function _validateField(value, rules, allValues) {
|
|
|
19
19
|
} else {
|
|
20
20
|
// Built-in validators
|
|
21
21
|
switch (name) {
|
|
22
|
+
case "required":
|
|
23
|
+
if (value == null || String(value).trim() === "")
|
|
24
|
+
return "Required";
|
|
25
|
+
break;
|
|
22
26
|
case "email":
|
|
23
27
|
if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
|
|
24
28
|
return "Invalid email";
|
|
@@ -90,6 +94,8 @@ registerDirective("validate", {
|
|
|
90
94
|
errors: {},
|
|
91
95
|
values: {},
|
|
92
96
|
reset: () => {
|
|
97
|
+
formCtx.dirty = false;
|
|
98
|
+
formCtx.touched = false;
|
|
93
99
|
el.reset();
|
|
94
100
|
checkValidity();
|
|
95
101
|
},
|
|
@@ -194,22 +200,32 @@ registerDirective("error-boundary", {
|
|
|
194
200
|
init(el, name, fallbackTpl) {
|
|
195
201
|
const ctx = findContext(el);
|
|
196
202
|
|
|
203
|
+
function showFallback(message) {
|
|
204
|
+
const clone = _cloneTemplate(fallbackTpl);
|
|
205
|
+
if (clone) {
|
|
206
|
+
const childCtx = createContext(
|
|
207
|
+
{ err: { message } },
|
|
208
|
+
ctx,
|
|
209
|
+
);
|
|
210
|
+
el.innerHTML = "";
|
|
211
|
+
const wrapper = document.createElement("div");
|
|
212
|
+
wrapper.style.display = "contents";
|
|
213
|
+
wrapper.__ctx = childCtx;
|
|
214
|
+
wrapper.appendChild(clone);
|
|
215
|
+
el.appendChild(wrapper);
|
|
216
|
+
processTree(wrapper);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Listen for NoJS expression errors dispatched from event handlers
|
|
221
|
+
el.addEventListener("nojs:error", (e) => {
|
|
222
|
+
showFallback(e.detail?.message || "An error occurred");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Listen for window-level errors (resource load failures, etc.)
|
|
197
226
|
window.addEventListener("error", (e) => {
|
|
198
227
|
if (el.contains(e.target) || el === e.target) {
|
|
199
|
-
|
|
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
|
-
}
|
|
228
|
+
showFallback(e.message || "An error occurred");
|
|
213
229
|
}
|
|
214
230
|
});
|
|
215
231
|
},
|
package/src/evaluate.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// EXPRESSION EVALUATOR
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════════════
|
|
4
4
|
|
|
5
|
-
import { _stores, _routerInstance, _filters, _warn, _config } from "./globals.js";
|
|
5
|
+
import { _stores, _routerInstance, _filters, _warn, _config, _notifyStoreWatchers } from "./globals.js";
|
|
6
6
|
import { _i18n } from "./i18n.js";
|
|
7
7
|
import { _collectKeys } from "./context.js";
|
|
8
8
|
|
|
@@ -272,6 +272,7 @@ export function _execStatement(expr, ctx, extraVars = {}) {
|
|
|
272
272
|
_wCtx = _wCtx.$parent;
|
|
273
273
|
}
|
|
274
274
|
const setters = [...chainKeys]
|
|
275
|
+
.filter((k) => !k.startsWith("$"))
|
|
275
276
|
.map(
|
|
276
277
|
(k) =>
|
|
277
278
|
`{let _c=__ctx;while(_c&&_c.__isProxy){if('${k}'in _c.__raw){_c.$set('${k}',typeof ${k}!=='undefined'?${k}:_c.__raw['${k}']);break;}_c=_c.$parent;}}`,
|
|
@@ -280,8 +281,19 @@ export function _execStatement(expr, ctx, extraVars = {}) {
|
|
|
280
281
|
|
|
281
282
|
const fn = new Function("__ctx", ...keyArr, `${expr};\n${setters}`);
|
|
282
283
|
fn(ctx, ...valArr);
|
|
284
|
+
|
|
285
|
+
// Notify global store watchers when expression touches $store
|
|
286
|
+
if (typeof expr === "string" && expr.includes("$store")) {
|
|
287
|
+
_notifyStoreWatchers();
|
|
288
|
+
}
|
|
283
289
|
} catch (e) {
|
|
284
290
|
_warn("Expression error:", expr, e.message);
|
|
291
|
+
// Dispatch a custom DOM event so error-boundary directives can catch it
|
|
292
|
+
if (extraVars.$el) {
|
|
293
|
+
extraVars.$el.dispatchEvent(
|
|
294
|
+
new CustomEvent("nojs:error", { bubbles: true, detail: { message: e.message, error: e } })
|
|
295
|
+
);
|
|
296
|
+
}
|
|
285
297
|
}
|
|
286
298
|
}
|
|
287
299
|
|
package/src/i18n.js
CHANGED
|
@@ -4,9 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import { _config } from "./globals.js";
|
|
6
6
|
|
|
7
|
+
const _i18nListeners = new Set();
|
|
8
|
+
|
|
9
|
+
export function _watchI18n(fn) {
|
|
10
|
+
_i18nListeners.add(fn);
|
|
11
|
+
return () => _i18nListeners.delete(fn);
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export const _i18n = {
|
|
8
|
-
|
|
15
|
+
_locale: "en",
|
|
9
16
|
locales: {},
|
|
17
|
+
get locale() {
|
|
18
|
+
return this._locale;
|
|
19
|
+
},
|
|
20
|
+
set locale(v) {
|
|
21
|
+
if (this._locale !== v) {
|
|
22
|
+
this._locale = v;
|
|
23
|
+
_i18nListeners.forEach((fn) => fn());
|
|
24
|
+
}
|
|
25
|
+
},
|
|
10
26
|
t(key, params = {}) {
|
|
11
27
|
const messages =
|
|
12
28
|
_i18n.locales[_i18n.locale] ||
|