@estjs/template 0.0.16-beta.1 → 0.0.16-beta.3

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.
@@ -1,6 +1,6 @@
1
1
  import { Signal, Computed } from '@estjs/signals';
2
- import { S as Scope } from './internal-Bz6h0aPa.cjs';
3
- export { I as InjectionKey, i as inject, p as provide } from './internal-Bz6h0aPa.cjs';
2
+ import { S as Scope } from './internal-DSKAj-zW.cjs';
3
+ export { I as InjectionKey, i as inject, p as provide } from './internal-DSKAj-zW.cjs';
4
4
  import { normalizeClassName } from '@estjs/shared';
5
5
 
6
6
  declare enum COMPONENT_STATE {
@@ -168,11 +168,11 @@ declare function onUpdate(hook: LifecycleHook): void;
168
168
  declare function onDestroy(hook: LifecycleHook): void;
169
169
 
170
170
  /**
171
- * Modifiers supported by `bind:*` two-way bindings.
171
+ * Modifiers for `bind:*` two-way bindings.
172
172
  *
173
- * - `trim` strip surrounding whitespace from string values
174
- * - `number` coerce numeric strings to numbers (no-op on `NaN`)
175
- * - `lazy` commit on `change` instead of `input`
173
+ * - `trim` strip surrounding whitespace
174
+ * - `number` coerce numeric strings to numbers (no-op on NaN)
175
+ * - `lazy` commit on `change` instead of `input`
176
176
  */
177
177
  interface BindModifiers {
178
178
  trim?: boolean;
@@ -181,15 +181,18 @@ interface BindModifiers {
181
181
  [key: string]: boolean | undefined;
182
182
  }
183
183
  /**
184
- * Synchronizes a DOM element property with a model getter and setter.
184
+ * Creates a two-way binding between a DOM element property and a reactive model.
185
185
  *
186
- * @param node The element to bind. `null` is tolerated (no-op).
187
- * @param prop Bound property: `value` / `checked` / `files` / arbitrary.
188
- * @param getter Reactive getter or static initial value.
189
- * @param setter Receives the new value when the user edits the DOM.
190
- * @param modifiers Optional `BindModifiers`.
186
+ * - **Model DOM** a reactive `effect()` pushes model changes to the element.
187
+ * - **DOM Model** an event listener reads user input and calls `setter`.
188
+ *
189
+ * @param node Target element. `null` is tolerated (no-op).
190
+ * @param prop Bound property (`value` / `checked` / `files` / custom).
191
+ * @param getter Reactive getter, or a static initial value.
192
+ * @param setter Called with the (optionally transformed) DOM value on user input.
193
+ * @param modifiers Optional `{ trim, number, lazy }`.
191
194
  */
192
- declare function bindElement(node: Element | null, prop: 'value' | 'checked' | 'files' | string, getter: (() => unknown) | unknown, setter: (value: unknown) => void, modifiers?: BindModifiers): void;
195
+ declare function bindElement(node: Element | null, prop: 'value' | 'checked' | 'files' | string, getter: (() => unknown) | unknown, setter: (v: unknown) => void, modifiers?: BindModifiers): void;
193
196
 
194
197
  /**
195
198
  * Set up event delegation for specified event types.
@@ -1,6 +1,6 @@
1
1
  import { Signal, Computed } from '@estjs/signals';
2
- import { S as Scope } from './internal-Bz6h0aPa.js';
3
- export { I as InjectionKey, i as inject, p as provide } from './internal-Bz6h0aPa.js';
2
+ import { S as Scope } from './internal-DSKAj-zW.js';
3
+ export { I as InjectionKey, i as inject, p as provide } from './internal-DSKAj-zW.js';
4
4
  import { normalizeClassName } from '@estjs/shared';
5
5
 
6
6
  declare enum COMPONENT_STATE {
@@ -168,11 +168,11 @@ declare function onUpdate(hook: LifecycleHook): void;
168
168
  declare function onDestroy(hook: LifecycleHook): void;
169
169
 
170
170
  /**
171
- * Modifiers supported by `bind:*` two-way bindings.
171
+ * Modifiers for `bind:*` two-way bindings.
172
172
  *
173
- * - `trim` strip surrounding whitespace from string values
174
- * - `number` coerce numeric strings to numbers (no-op on `NaN`)
175
- * - `lazy` commit on `change` instead of `input`
173
+ * - `trim` strip surrounding whitespace
174
+ * - `number` coerce numeric strings to numbers (no-op on NaN)
175
+ * - `lazy` commit on `change` instead of `input`
176
176
  */
177
177
  interface BindModifiers {
178
178
  trim?: boolean;
@@ -181,15 +181,18 @@ interface BindModifiers {
181
181
  [key: string]: boolean | undefined;
182
182
  }
183
183
  /**
184
- * Synchronizes a DOM element property with a model getter and setter.
184
+ * Creates a two-way binding between a DOM element property and a reactive model.
185
185
  *
186
- * @param node The element to bind. `null` is tolerated (no-op).
187
- * @param prop Bound property: `value` / `checked` / `files` / arbitrary.
188
- * @param getter Reactive getter or static initial value.
189
- * @param setter Receives the new value when the user edits the DOM.
190
- * @param modifiers Optional `BindModifiers`.
186
+ * - **Model DOM** a reactive `effect()` pushes model changes to the element.
187
+ * - **DOM Model** an event listener reads user input and calls `setter`.
188
+ *
189
+ * @param node Target element. `null` is tolerated (no-op).
190
+ * @param prop Bound property (`value` / `checked` / `files` / custom).
191
+ * @param getter Reactive getter, or a static initial value.
192
+ * @param setter Called with the (optionally transformed) DOM value on user input.
193
+ * @param modifiers Optional `{ trim, number, lazy }`.
191
194
  */
192
- declare function bindElement(node: Element | null, prop: 'value' | 'checked' | 'files' | string, getter: (() => unknown) | unknown, setter: (value: unknown) => void, modifiers?: BindModifiers): void;
195
+ declare function bindElement(node: Element | null, prop: 'value' | 'checked' | 'files' | string, getter: (() => unknown) | unknown, setter: (v: unknown) => void, modifiers?: BindModifiers): void;
193
196
 
194
197
  /**
195
198
  * Set up event delegation for specified event types.
@@ -19,7 +19,7 @@ var __objRest = (source, exclude) => {
19
19
  return target;
20
20
  };
21
21
  var __async = (__this, __arguments, generator) => {
22
- return new Promise((resolve, reject) => {
22
+ return new Promise((resolve2, reject) => {
23
23
  var fulfilled = (value) => {
24
24
  try {
25
25
  step(generator.next(value));
@@ -34,7 +34,7 @@ var __async = (__this, __arguments, generator) => {
34
34
  reject(e);
35
35
  }
36
36
  };
37
- var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
37
+ var step = (x) => x.done ? resolve2(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
38
38
  step((generator = generator.apply(__this, __arguments)).next());
39
39
  });
40
40
  };
@@ -56,8 +56,10 @@ function getActiveScope() {
56
56
  return activeScope;
57
57
  }
58
58
  function createScope(parent = activeScope) {
59
+ const reactiveScope = parent ? parent.effectScope.run(() => signals.effectScope()) : signals.effectScope(true);
59
60
  const scope = {
60
61
  id: ++scopeId,
62
+ effectScope: reactiveScope,
61
63
  parent,
62
64
  children: null,
63
65
  // Lazy initialized
@@ -86,7 +88,7 @@ function runWithScope(scope, fn) {
86
88
  const prevScope = activeScope;
87
89
  activeScope = scope;
88
90
  try {
89
- return fn();
91
+ return scope.effectScope.run(fn);
90
92
  } finally {
91
93
  activeScope = prevScope;
92
94
  }
@@ -106,15 +108,13 @@ function disposeScope(scope) {
106
108
  }
107
109
  scope.children.clear();
108
110
  }
109
- const prevScope = activeScope;
110
- activeScope = scope;
111
- try {
111
+ runWithScope(scope, () => {
112
112
  if (scope.onDestroy) {
113
113
  for (let i = 0; i < scope.onDestroy.length; i++) {
114
114
  try {
115
115
  scope.onDestroy[i]();
116
116
  } catch (error_) {
117
- if (true) {
117
+ {
118
118
  shared.error(`Scope(${scope.id}): Error in destroy hook:`, error_);
119
119
  }
120
120
  }
@@ -126,16 +126,15 @@ function disposeScope(scope) {
126
126
  try {
127
127
  scope.cleanup[i]();
128
128
  } catch (error_) {
129
- if (true) {
129
+ {
130
130
  shared.error(`Scope(${scope.id}): Error in cleanup:`, error_);
131
131
  }
132
132
  }
133
133
  }
134
134
  scope.cleanup = null;
135
135
  }
136
- } finally {
137
- activeScope = prevScope;
138
- }
136
+ });
137
+ scope.effectScope.stop();
139
138
  if ((_a2 = scope.parent) == null ? void 0 : _a2.children) {
140
139
  scope.parent.children.delete(scope);
141
140
  }
@@ -207,7 +206,7 @@ function patchAttr(el, key, prev, next2) {
207
206
  const elementIsSVG = (el == null ? void 0 : el.namespaceURI) === SVG_NAMESPACE;
208
207
  const isXlink = elementIsSVG && key.startsWith("xlink:");
209
208
  const isXmlns = elementIsSVG && key.startsWith("xmlns:");
210
- const isBoolean2 = shared.isSpecialBooleanAttr(key) || shared.isBooleanAttr(key);
209
+ const isBoolean = shared.isSpecialBooleanAttr(key) || shared.isBooleanAttr(key);
211
210
  if (prev === next2) {
212
211
  return;
213
212
  }
@@ -232,7 +231,7 @@ function patchAttr(el, key, prev, next2) {
232
231
  }
233
232
  return;
234
233
  }
235
- if (isBoolean2) {
234
+ if (isBoolean) {
236
235
  if (shared.includeBooleanAttr(next2)) {
237
236
  el.setAttribute(key, "");
238
237
  } else {
@@ -676,22 +675,30 @@ function insertNode(parent, child2, before) {
676
675
  }
677
676
  }
678
677
  function normalizeNode(node) {
679
- if (shared.isHTMLElement(node)) {
680
- return node;
681
- }
682
- if (shared.isPrimitive(node)) {
683
- return document.createTextNode(shared.isFalsy(node) ? "" : String(node));
678
+ if (node instanceof Node) return node;
679
+ if (isComponent(node)) return node;
680
+ const t = typeof node;
681
+ if (node == null || t === "string" || t === "number" || t === "boolean" || t === "symbol") {
682
+ return document.createTextNode(node === false || node == null ? "" : String(node));
683
+ }
684
+ if (shared.isObject(node)) {
685
+ shared.warn(
686
+ "Rendering a plain object as a node is not recommended. The object will be converted to its string representation.",
687
+ node
688
+ );
684
689
  }
685
- return node;
690
+ return document.createTextNode(String(node));
686
691
  }
687
692
  function insert(parent, nodeFactory, before) {
688
693
  if (!parent) return;
689
- const ownerScope = getActiveScope();
694
+ const parentScope = getActiveScope();
690
695
  let renderedNodes = [];
691
696
  let isFirstRun = true;
692
697
  const resolveNodes = (raw) => {
693
698
  if (raw instanceof Node) return [raw];
694
- if (shared.isNull(raw) || shared.isUndefined(raw) || shared.isString(raw) || shared.isNumber(raw) || shared.isBoolean(raw)) {
699
+ if (isComponent(raw)) return [raw];
700
+ const t = typeof raw;
701
+ if (raw == null || t === "string" || t === "number" || t === "boolean") {
695
702
  return [normalizeNode(raw)];
696
703
  }
697
704
  return shared.coerceArray(raw).map((item) => shared.isFunction(item) ? item() : item).flatMap((i) => i).map(normalizeNode);
@@ -708,8 +715,8 @@ function insert(parent, nodeFactory, before) {
708
715
  renderedNodes = reconcileArrays(parent, renderedNodes, nodes, before);
709
716
  isFirstRun = false;
710
717
  };
711
- if (ownerScope && !ownerScope.isDestroyed) {
712
- runWithScope(ownerScope, executeUpdate);
718
+ if (parentScope && !parentScope.isDestroyed) {
719
+ runWithScope(parentScope, executeUpdate);
713
720
  } else {
714
721
  executeUpdate();
715
722
  }
@@ -910,7 +917,9 @@ var Component = class {
910
917
  if (shared.isFunction(result)) {
911
918
  result = result(this.reactiveProps);
912
919
  }
913
- if (signals.isSignal(result) || signals.isComputed(result)) {
920
+ if (signals.isSignal(result)) {
921
+ result = result.value;
922
+ } else if (signals.isComputed(result)) {
914
923
  result = result.value;
915
924
  }
916
925
  return (_a3 = insert(parentNode, result, beforeNode)) != null ? _a3 : [];
@@ -1011,11 +1020,12 @@ var Component = class {
1011
1020
  return () => value(null);
1012
1021
  }
1013
1022
  if (signals.isSignal(value)) {
1014
- const previousValue = value.value;
1015
- value.value = root;
1023
+ const ref = value;
1024
+ const previousValue = ref.value;
1025
+ ref.value = root;
1016
1026
  return () => {
1017
- if (value.value === root) {
1018
- value.value = previousValue;
1027
+ if (ref.value === root) {
1028
+ ref.value = previousValue;
1019
1029
  }
1020
1030
  };
1021
1031
  }
@@ -1230,154 +1240,144 @@ function addEventListener(element, event, handler, options) {
1230
1240
  }
1231
1241
 
1232
1242
  // src/binding.ts
1233
- var INPUT_CHECKBOX_CHECKED = {
1243
+ function writeValue(el, v) {
1244
+ const target = el;
1245
+ const next2 = v == null ? "" : String(v);
1246
+ if (target.value !== next2) target.value = next2;
1247
+ }
1248
+ var CHECKBOX = {
1234
1249
  event: "change",
1235
- forceChangeEvent: true,
1236
- read: (n) => n.checked,
1237
- write: (n, v) => {
1238
- const el = n;
1250
+ forceChange: true,
1251
+ read: (el) => el.checked,
1252
+ write(el, v) {
1253
+ const e = el;
1239
1254
  const next2 = Boolean(v);
1240
- if (el.checked !== next2) el.checked = next2;
1255
+ if (e.checked !== next2) e.checked = next2;
1241
1256
  }
1242
1257
  };
1243
- var INPUT_RADIO_CHECKED = {
1258
+ var RADIO = {
1244
1259
  event: "change",
1245
- forceChangeEvent: true,
1246
- read: (n) => {
1247
- const el = n;
1248
- return el.checked ? el.value : "";
1260
+ forceChange: true,
1261
+ read(el) {
1262
+ const e = el;
1263
+ return e.checked ? e.value : "";
1249
1264
  },
1250
- write: (n, v) => {
1251
- const el = n;
1252
- const next2 = String(v) === el.value;
1253
- if (el.checked !== next2) el.checked = next2;
1265
+ write(el, v) {
1266
+ const e = el;
1267
+ const next2 = String(v) === e.value;
1268
+ if (e.checked !== next2) e.checked = next2;
1254
1269
  }
1255
1270
  };
1256
- var INPUT_FILE_FILES = {
1271
+ var FILE = {
1257
1272
  event: "change",
1258
- forceChangeEvent: true,
1259
- read: (n) => n.files,
1260
- // Browsers do not allow programmatic writes to <input type="file">.
1261
- write: () => {
1273
+ forceChange: true,
1274
+ read: (el) => el.files,
1275
+ write() {
1262
1276
  }
1277
+ // browsers forbid programmatic writes to file inputs
1263
1278
  };
1264
- var INPUT_VALUE = {
1279
+ var TEXT = {
1265
1280
  event: "input",
1266
- needsComposition: true,
1267
- read: (n) => n.value,
1268
- write: (n, v) => {
1269
- const el = n;
1270
- const next2 = v == null ? "" : String(v);
1271
- if (el.value !== next2) el.value = next2;
1272
- }
1281
+ ime: true,
1282
+ read: (el) => el.value,
1283
+ write: writeValue
1284
+ };
1285
+ var TEXTAREA = {
1286
+ event: "input",
1287
+ ime: true,
1288
+ read: (el) => el.value,
1289
+ write: writeValue
1273
1290
  };
1274
- var SELECT_VALUE = {
1291
+ var SELECT = {
1275
1292
  event: "change",
1276
- forceChangeEvent: true,
1277
- read: (n) => {
1278
- const s = n;
1293
+ forceChange: true,
1294
+ read(el) {
1295
+ const s = el;
1279
1296
  return s.multiple ? Array.from(s.selectedOptions, (o) => o.value) : s.value;
1280
1297
  },
1281
- write: (n, v) => {
1282
- const s = n;
1298
+ write(el, v) {
1299
+ const s = el;
1283
1300
  if (s.multiple) {
1284
- const set = new Set((Array.isArray(v) ? v : []).map(String));
1285
- for (const opt of Array.from(s.options)) opt.selected = set.has(opt.value);
1301
+ const selected = new Set((Array.isArray(v) ? v : []).map(String));
1302
+ for (const opt of Array.from(s.options)) opt.selected = selected.has(opt.value);
1286
1303
  } else {
1287
- const next2 = v == null ? "" : String(v);
1288
- if (s.value !== next2) s.value = next2;
1304
+ writeValue(el, v);
1289
1305
  }
1290
1306
  }
1291
1307
  };
1292
- var TEXTAREA_VALUE = {
1293
- event: "input",
1294
- needsComposition: true,
1295
- read: (n) => n.value,
1296
- write: (n, v) => {
1297
- const el = n;
1298
- const next2 = v == null ? "" : String(v);
1299
- if (el.value !== next2) el.value = next2;
1308
+ function resolve(node, prop) {
1309
+ switch (node.nodeName) {
1310
+ case "INPUT":
1311
+ if (prop === "checked") return node.type === "radio" ? RADIO : CHECKBOX;
1312
+ if (prop === "files") return FILE;
1313
+ return TEXT;
1314
+ case "SELECT":
1315
+ return SELECT;
1316
+ case "TEXTAREA":
1317
+ return TEXTAREA;
1318
+ default:
1319
+ return {
1320
+ event: "input",
1321
+ read: (el) => el[prop],
1322
+ write(el, v) {
1323
+ el[prop] = v;
1324
+ }
1325
+ };
1300
1326
  }
1301
- };
1302
- function resolveStrategy(node, prop) {
1303
- const tag = node.nodeName;
1304
- if (tag === "INPUT") {
1305
- const type = node.type;
1306
- if (prop === "checked") {
1307
- return type === "radio" ? INPUT_RADIO_CHECKED : INPUT_CHECKBOX_CHECKED;
1308
- }
1309
- if (prop === "files") return INPUT_FILE_FILES;
1310
- return INPUT_VALUE;
1311
- }
1312
- if (tag === "SELECT") return SELECT_VALUE;
1313
- if (tag === "TEXTAREA") return TEXTAREA_VALUE;
1314
- return {
1315
- event: "input",
1316
- read: (n) => n[prop],
1317
- write: (n, v) => {
1318
- n[prop] = v;
1319
- }
1320
- };
1321
1327
  }
1322
- function castValue(val, trim, number) {
1323
- if (!shared.isString(val)) return val;
1324
- if (trim) val = val.trim();
1325
- if (number && val !== "") {
1326
- const parsed = Number(val);
1327
- if (!Number.isNaN(parsed)) return parsed;
1328
+ function applyModifiers(v, trim, toNum) {
1329
+ if (!shared.isString(v)) return v;
1330
+ let s = v;
1331
+ if (trim) s = s.trim();
1332
+ if (toNum && s !== "") {
1333
+ const n = Number(s);
1334
+ if (!Number.isNaN(n)) return n;
1328
1335
  }
1329
- return val;
1336
+ return s;
1330
1337
  }
1331
- function isFocused(node) {
1332
- const root = node.getRootNode();
1333
- return (root instanceof Document || root instanceof ShadowRoot) && root.activeElement === node;
1338
+ function isFocused(el) {
1339
+ const root = el.getRootNode();
1340
+ return (root instanceof Document || root instanceof ShadowRoot) && root.activeElement === el;
1334
1341
  }
1335
1342
  function bindElement(node, prop, getter, setter, modifiers = {}) {
1336
1343
  if (!node) return;
1337
- const strategy = resolveStrategy(node, prop);
1338
- const { trim, number, lazy } = modifiers;
1339
- const isFiles = prop === "files";
1340
- const readModel = () => shared.isFunction(getter) ? getter() : getter;
1341
- const transform = (v) => isFiles ? v : castValue(v, trim, number);
1342
- const eventName = lazy || strategy.forceChangeEvent ? "change" : strategy.event;
1344
+ const { event, read, write, forceChange, ime } = resolve(node, prop);
1345
+ const trim = modifiers.trim === true;
1346
+ const toNum = modifiers.number === true;
1347
+ const lazy = modifiers.lazy === true;
1348
+ const shouldCast = (trim || toNum) && prop !== "files";
1349
+ const getModel = shared.isFunction(getter) ? getter : () => getter;
1350
+ const cast = shouldCast ? (v) => applyModifiers(v, trim, toNum) : (v) => v;
1343
1351
  let composing = false;
1344
- const syncFromDom = () => {
1352
+ const eventName = lazy || forceChange ? "change" : event;
1353
+ const syncToModel = () => {
1345
1354
  if (composing) return;
1346
- const raw = strategy.read(node);
1355
+ const raw = read(node);
1347
1356
  if (raw === void 0) return;
1348
- if (isFiles) {
1349
- setter(raw);
1350
- return;
1351
- }
1352
- const next2 = transform(raw);
1353
- if (!Object.is(readModel(), next2)) {
1354
- setter(next2);
1355
- }
1357
+ const next2 = cast(raw);
1358
+ if (!Object.is(getModel(), next2)) setter(next2);
1356
1359
  };
1357
- addEventListener(node, eventName, syncFromDom);
1358
- if (!lazy && !isFiles && (trim || number) && eventName !== "change") {
1359
- addEventListener(node, "change", () => {
1360
- strategy.write(node, transform(strategy.read(node)));
1361
- });
1360
+ addEventListener(node, eventName, syncToModel);
1361
+ if (!lazy && shouldCast && eventName !== "change") {
1362
+ addEventListener(node, "change", () => write(node, cast(read(node))));
1362
1363
  }
1363
- if (strategy.needsComposition && !lazy) {
1364
+ if (ime && !lazy) {
1364
1365
  addEventListener(node, "compositionstart", () => {
1365
1366
  composing = true;
1366
1367
  });
1367
1368
  addEventListener(node, "compositionend", () => {
1368
1369
  if (!composing) return;
1369
1370
  composing = false;
1370
- syncFromDom();
1371
+ syncToModel();
1371
1372
  });
1372
1373
  }
1373
1374
  const runner = signals.effect(() => {
1374
- const value = readModel();
1375
- if (strategy.needsComposition && !lazy && isFocused(node)) {
1375
+ const value = getModel();
1376
+ if (ime && !lazy && isFocused(node)) {
1376
1377
  if (composing) return;
1377
- const current = transform(strategy.read(node));
1378
- if (Object.is(current, value)) return;
1378
+ if (Object.is(cast(read(node)), value)) return;
1379
1379
  }
1380
- strategy.write(node, value);
1380
+ write(node, value);
1381
1381
  });
1382
1382
  if (getActiveScope()) {
1383
1383
  onCleanup(() => runner.stop());
@@ -1451,10 +1451,10 @@ function Portal(props) {
1451
1451
  placeholder[PORTAL_COMPONENT] = true;
1452
1452
  const { children } = props;
1453
1453
  if (children == null) return placeholder;
1454
- const ownerScope = getActiveScope();
1454
+ const parentScope = getActiveScope();
1455
1455
  let innerScope = null;
1456
1456
  const mountAt = (parent, before) => {
1457
- innerScope = createScope(ownerScope);
1457
+ innerScope = createScope(parentScope);
1458
1458
  runWithScope(innerScope, () => {
1459
1459
  insert(parent, () => children, before);
1460
1460
  });