@nisli/core 0.57.0 → 0.57.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/README.md +32 -3
- package/dist/bundler/template.js +107 -32
- package/dist/bundler/template.js.map +2 -2
- package/dist/index.js +107 -32
- package/dist/index.js.map +2 -2
- package/dist/index.production.js +1 -1
- package/dist/index.production.js.map +2 -2
- package/dist/types/template.d.ts +2 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@ npm install @nisli/core
|
|
|
14
14
|
`@nisli/core` has no runtime dependencies. It works with TypeScript or plain
|
|
15
15
|
JavaScript and does not require a framework-specific build step.
|
|
16
16
|
|
|
17
|
+
See the [framework tenets](https://github.com/gkoreli/nisli/blob/main/docs/TENETS.md)
|
|
18
|
+
for design constraints.
|
|
19
|
+
|
|
17
20
|
## Quick start
|
|
18
21
|
|
|
19
22
|
```ts
|
|
@@ -36,6 +39,30 @@ Signals are read with `.value` in TypeScript and passed directly to templates.
|
|
|
36
39
|
Only the bound text, attribute, class, or child slot updates when a signal
|
|
37
40
|
changes.
|
|
38
41
|
|
|
42
|
+
## Native form bindings
|
|
43
|
+
|
|
44
|
+
`html` and `el()` synchronize native `value` (input, textarea, select), `checked`
|
|
45
|
+
(input), and `selected` (option) properties as well as their attributes. An
|
|
46
|
+
external signal change therefore replaces a user-edited value without a ref or
|
|
47
|
+
an application effect:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const name = signal('');
|
|
51
|
+
html`<input value=${name} @input=${(event: Event) => {
|
|
52
|
+
name.value = (event.target as HTMLInputElement).value;
|
|
53
|
+
}}>`;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Bindings remain one-way; native events update your model only through handlers.
|
|
57
|
+
Equal text-value writes preserve the caret. Select values initialize after
|
|
58
|
+
options and reconcile in a mutation-observer microtask when options change.
|
|
59
|
+
A select value is scalar; bind option `selected` states for multiple selections.
|
|
60
|
+
Nonempty file values cannot populate uploads; an empty value can clear them.
|
|
61
|
+
|
|
62
|
+
Form reset keeps native defaults: input value/checked and option selected
|
|
63
|
+
attributes, textarea child text, and select option defaults. Reset does not write
|
|
64
|
+
signals. Ordinary attributes and custom-element props keep their existing paths.
|
|
65
|
+
|
|
39
66
|
## Components that also work as HTML
|
|
40
67
|
|
|
41
68
|
`component()` registers a standard custom element and returns its typed factory.
|
|
@@ -264,9 +291,11 @@ per-key DOM identity does the rest, with no generated names:
|
|
|
264
291
|
- Safe light-DOM templates: signal bindings, typed events and modifiers,
|
|
265
292
|
`class:*`, branded `html:inner` (trusted or sanitizer-routed), refs, and
|
|
266
293
|
dynamic HTML tags through `el()`.
|
|
267
|
-
- Stable control flow: lazy `when()` branches and keyed `each()` lists
|
|
268
|
-
|
|
269
|
-
|
|
294
|
+
- Stable control flow: lazy `when()` branches and keyed `each()` lists retain
|
|
295
|
+
component state and DOM identity. Reorders use `moveBefore()` where available;
|
|
296
|
+
the fallback keeps a retained focused row attached without refocusing it.
|
|
297
|
+
Other moved rows can lose native state such as iframe documents or popovers
|
|
298
|
+
on the fallback path; document Selection ranges are not preserved.
|
|
270
299
|
- Two scopes of dependency injection: app-wide singleton services with
|
|
271
300
|
`inject`/`provide`, and portal-safe subtree state with `createContext`.
|
|
272
301
|
- Lifecycle and resilience: `onMount`, `onCleanup`, `useHostEvent`, automatic
|
package/dist/bundler/template.js
CHANGED
|
@@ -105,8 +105,10 @@ function html(strings, ...values) {
|
|
|
105
105
|
mounted = true;
|
|
106
106
|
const template = parseTemplate(strings, values.length);
|
|
107
107
|
const fragment = template.content.cloneNode(true);
|
|
108
|
-
|
|
108
|
+
const afterMount = [];
|
|
109
|
+
processNode(fragment, values, bindings, disposers, afterMount);
|
|
109
110
|
host.appendChild(fragment);
|
|
111
|
+
for (const sync of afterMount) sync();
|
|
110
112
|
},
|
|
111
113
|
dispose() {
|
|
112
114
|
for (const d of disposers) {
|
|
@@ -129,10 +131,11 @@ function html(strings, ...values) {
|
|
|
129
131
|
}
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
|
-
function processNode(node, values, bindings, disposers) {
|
|
134
|
+
function processNode(node, values, bindings, disposers, afterMount) {
|
|
135
|
+
let afterChildren;
|
|
133
136
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
134
137
|
const el2 = node;
|
|
135
|
-
processAttributes(el2, values, bindings, disposers);
|
|
138
|
+
afterChildren = processAttributes(el2, values, bindings, disposers, afterMount);
|
|
136
139
|
}
|
|
137
140
|
if (node.nodeType === Node.COMMENT_NODE) {
|
|
138
141
|
const comment = node;
|
|
@@ -146,11 +149,13 @@ function processNode(node, values, bindings, disposers) {
|
|
|
146
149
|
}
|
|
147
150
|
const children = [...node.childNodes];
|
|
148
151
|
for (const child of children) {
|
|
149
|
-
processNode(child, values, bindings, disposers);
|
|
152
|
+
processNode(child, values, bindings, disposers, afterMount);
|
|
150
153
|
}
|
|
154
|
+
if (afterChildren) for (const bind of afterChildren) bind();
|
|
151
155
|
}
|
|
152
|
-
function processAttributes(el2, values, bindings, disposers) {
|
|
156
|
+
function processAttributes(el2, values, bindings, disposers, afterMount) {
|
|
153
157
|
const attrsToRemove = [];
|
|
158
|
+
let afterChildren;
|
|
154
159
|
for (const attr of [...el2.attributes]) {
|
|
155
160
|
const name = attr.name;
|
|
156
161
|
const attrValue = attr.value;
|
|
@@ -209,8 +214,9 @@ function processAttributes(el2, values, bindings, disposers) {
|
|
|
209
214
|
const markers = [...attrValue.matchAll(/<!--bk-(\d+)-->/g)];
|
|
210
215
|
if (markers.length) {
|
|
211
216
|
const first = markers[0];
|
|
217
|
+
let value;
|
|
212
218
|
if (markers.length === 1 && first && attrValue === first[0]) {
|
|
213
|
-
|
|
219
|
+
value = values[Number(first[1])];
|
|
214
220
|
} else {
|
|
215
221
|
const resolve = () => attrValue.replace(/<!--bk-(\d+)-->/g, (_, i) => {
|
|
216
222
|
const v = values[Number(i)];
|
|
@@ -218,7 +224,12 @@ function processAttributes(el2, values, bindings, disposers) {
|
|
|
218
224
|
return raw2 == null || raw2 === false ? "" : String(raw2);
|
|
219
225
|
});
|
|
220
226
|
const hasSignals = markers.some((m) => isSignal(values[Number(m[1])]));
|
|
221
|
-
|
|
227
|
+
value = hasSignals ? computed(resolve) : resolve();
|
|
228
|
+
}
|
|
229
|
+
if (nativeFormProperty(el2, name)) {
|
|
230
|
+
(afterChildren ??= []).push(() => bindAttribute(el2, name, value, bindings, disposers, afterMount));
|
|
231
|
+
} else {
|
|
232
|
+
bindAttribute(el2, name, value, bindings, disposers, afterMount);
|
|
222
233
|
}
|
|
223
234
|
}
|
|
224
235
|
continue;
|
|
@@ -227,6 +238,7 @@ function processAttributes(el2, values, bindings, disposers) {
|
|
|
227
238
|
for (const name of attrsToRemove) {
|
|
228
239
|
el2.removeAttribute(name);
|
|
229
240
|
}
|
|
241
|
+
return afterChildren;
|
|
230
242
|
}
|
|
231
243
|
function mountFactoryResult(factory) {
|
|
232
244
|
const el2 = document.createElement(factory.tagName);
|
|
@@ -425,7 +437,7 @@ function replaceMarkerWithBinding(comment, value, bindings, disposers) {
|
|
|
425
437
|
parent.replaceChild(textNode, comment);
|
|
426
438
|
}
|
|
427
439
|
}
|
|
428
|
-
function bindAttribute(el2, name, value, bindings, disposers) {
|
|
440
|
+
function bindAttribute(el2, name, value, bindings, disposers, afterMount) {
|
|
429
441
|
if (name === "class") {
|
|
430
442
|
bindClassAttribute(el2, value, bindings, disposers);
|
|
431
443
|
return;
|
|
@@ -446,32 +458,69 @@ function bindAttribute(el2, name, value, bindings, disposers) {
|
|
|
446
458
|
}
|
|
447
459
|
return;
|
|
448
460
|
}
|
|
449
|
-
bindPlainAttribute(el2, name, value, bindings, disposers);
|
|
461
|
+
bindPlainAttribute(el2, name, value, bindings, disposers, afterMount);
|
|
450
462
|
}
|
|
451
|
-
function bindPlainAttribute(el2, name, value, bindings, disposers) {
|
|
463
|
+
function bindPlainAttribute(el2, name, value, bindings, disposers, afterMount) {
|
|
464
|
+
const property = nativeFormProperty(el2, name);
|
|
465
|
+
let current;
|
|
466
|
+
const apply = (v) => {
|
|
467
|
+
current = v;
|
|
468
|
+
if (v == null || v === false) {
|
|
469
|
+
el2.removeAttribute(name);
|
|
470
|
+
} else {
|
|
471
|
+
el2.setAttribute(name, v === true ? "" : String(v));
|
|
472
|
+
}
|
|
473
|
+
if (property) syncFormProperty(el2, property, v);
|
|
474
|
+
};
|
|
452
475
|
if (isSignal(value)) {
|
|
453
476
|
const binding = { type: "attribute", element: el2, name };
|
|
454
477
|
bindings.push(binding);
|
|
455
|
-
const dispose = effect(() =>
|
|
456
|
-
const v = value.value;
|
|
457
|
-
if (v == null || v === false) {
|
|
458
|
-
el2.removeAttribute(name);
|
|
459
|
-
} else if (v === true) {
|
|
460
|
-
el2.setAttribute(name, "");
|
|
461
|
-
} else {
|
|
462
|
-
el2.setAttribute(name, String(v));
|
|
463
|
-
}
|
|
464
|
-
});
|
|
478
|
+
const dispose = effect(() => apply(value.value));
|
|
465
479
|
binding.dispose = dispose;
|
|
466
480
|
disposers.push(dispose);
|
|
467
481
|
} else {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
el2
|
|
474
|
-
}
|
|
482
|
+
apply(value);
|
|
483
|
+
}
|
|
484
|
+
if (property === "value" && el2.localName === "select") {
|
|
485
|
+
let active = true;
|
|
486
|
+
const sync = () => {
|
|
487
|
+
if (active) syncFormProperty(el2, "value", current);
|
|
488
|
+
};
|
|
489
|
+
const observer = new MutationObserver(sync);
|
|
490
|
+
observer.observe(el2, {
|
|
491
|
+
childList: true,
|
|
492
|
+
subtree: true,
|
|
493
|
+
characterData: true,
|
|
494
|
+
attributes: true,
|
|
495
|
+
attributeFilter: ["value", "selected"]
|
|
496
|
+
});
|
|
497
|
+
disposers.push(() => {
|
|
498
|
+
active = false;
|
|
499
|
+
observer.disconnect();
|
|
500
|
+
});
|
|
501
|
+
afterMount.push(sync);
|
|
502
|
+
queueMicrotask(sync);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function nativeFormProperty(el2, name) {
|
|
506
|
+
const attribute = name.toLowerCase();
|
|
507
|
+
if (attribute !== "value" && attribute !== "checked" && attribute !== "selected") return;
|
|
508
|
+
if (el2.namespaceURI !== "http://www.w3.org/1999/xhtml") return;
|
|
509
|
+
const tag = el2.localName;
|
|
510
|
+
if (attribute === "value" && (tag === "input" || tag === "textarea" || tag === "select")) return attribute;
|
|
511
|
+
if (attribute === "checked" && tag === "input") return attribute;
|
|
512
|
+
if (attribute === "selected" && tag === "option") return attribute;
|
|
513
|
+
}
|
|
514
|
+
function syncFormProperty(el2, property, value) {
|
|
515
|
+
if (property === "value") {
|
|
516
|
+
const control = el2;
|
|
517
|
+
const next = value == null || typeof value === "boolean" ? "" : String(value);
|
|
518
|
+
if (el2.localName === "input" && el2.type === "file" && next !== "") return;
|
|
519
|
+
if (el2.localName === "select" || control.value !== next) control.value = next;
|
|
520
|
+
} else {
|
|
521
|
+
const control = el2;
|
|
522
|
+
const next = value != null && value !== false;
|
|
523
|
+
if (control[property] !== next) control[property] = next;
|
|
475
524
|
}
|
|
476
525
|
}
|
|
477
526
|
function bindClassAttribute(el2, value, bindings, disposers) {
|
|
@@ -728,11 +777,28 @@ function each(items, keyFn, templateFn) {
|
|
|
728
777
|
entry.wrapper.parentNode?.removeChild(entry.wrapper);
|
|
729
778
|
}
|
|
730
779
|
}
|
|
731
|
-
|
|
732
|
-
|
|
780
|
+
const atomic = canMoveWithin(parent);
|
|
781
|
+
let focusedIndex = -1;
|
|
782
|
+
if (!atomic) {
|
|
783
|
+
const root = parent.getRootNode();
|
|
784
|
+
let active = "activeElement" in root ? root.activeElement : null;
|
|
785
|
+
while (active && active.parentNode !== parent) active = active.parentElement;
|
|
786
|
+
if (active) focusedIndex = newEntries.findIndex((entry) => entry.wrapper === active);
|
|
787
|
+
}
|
|
788
|
+
if (focusedIndex !== -1) {
|
|
789
|
+
let next = newEntries[focusedIndex].wrapper;
|
|
790
|
+
for (let i = focusedIndex - 1; i >= 0; i--) {
|
|
791
|
+
const wrapper = newEntries[i].wrapper;
|
|
792
|
+
if (wrapper.nextSibling !== next) parent.insertBefore(wrapper, next);
|
|
793
|
+
next = wrapper;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
let cursor = focusedIndex === -1 ? startMarker : newEntries[focusedIndex].wrapper;
|
|
797
|
+
for (let i = focusedIndex + 1; i < newEntries.length; i++) {
|
|
798
|
+
const entry = newEntries[i];
|
|
733
799
|
const nextSibling = cursor.nextSibling;
|
|
734
800
|
if (nextSibling !== entry.wrapper) {
|
|
735
|
-
if (entry.wrapper.parentNode === parent &&
|
|
801
|
+
if (entry.wrapper.parentNode === parent && atomic) {
|
|
736
802
|
parent.moveBefore(entry.wrapper, nextSibling);
|
|
737
803
|
} else {
|
|
738
804
|
parent.insertBefore(entry.wrapper, nextSibling);
|
|
@@ -772,8 +838,11 @@ function el(tag, props = {}, children) {
|
|
|
772
838
|
mount(host) {
|
|
773
839
|
const element = document.createElement(tag);
|
|
774
840
|
node = element;
|
|
841
|
+
let afterChildren;
|
|
842
|
+
const afterMount = [];
|
|
775
843
|
for (const [key, val] of Object.entries(props)) {
|
|
776
|
-
|
|
844
|
+
const formProperty = nativeFormProperty(element, key);
|
|
845
|
+
if (val === void 0 && !formProperty) continue;
|
|
777
846
|
if (key === "ref") {
|
|
778
847
|
if (isRef(val)) {
|
|
779
848
|
val.current = element;
|
|
@@ -793,7 +862,11 @@ function el(tag, props = {}, children) {
|
|
|
793
862
|
bindClassAttribute(element, val, bindings, disposers);
|
|
794
863
|
continue;
|
|
795
864
|
}
|
|
796
|
-
|
|
865
|
+
if (formProperty) {
|
|
866
|
+
(afterChildren ??= []).push(() => bindPlainAttribute(element, key, val, bindings, disposers, afterMount));
|
|
867
|
+
} else {
|
|
868
|
+
bindPlainAttribute(element, key, val, bindings, disposers, afterMount);
|
|
869
|
+
}
|
|
797
870
|
}
|
|
798
871
|
const flat = [];
|
|
799
872
|
flattenChildren(children, flat);
|
|
@@ -802,7 +875,9 @@ function el(tag, props = {}, children) {
|
|
|
802
875
|
element.appendChild(anchor);
|
|
803
876
|
replaceMarkerWithBinding(anchor, child, bindings, disposers);
|
|
804
877
|
}
|
|
878
|
+
if (afterChildren) for (const bind of afterChildren) bind();
|
|
805
879
|
host.appendChild(element);
|
|
880
|
+
for (const sync of afterMount) sync();
|
|
806
881
|
},
|
|
807
882
|
dispose() {
|
|
808
883
|
for (const d of disposers) {
|