@estjs/template 0.0.16-beta.1 → 0.0.16-beta.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.
@@ -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.
@@ -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
  };
@@ -207,7 +207,7 @@ function patchAttr(el, key, prev, next2) {
207
207
  const elementIsSVG = (el == null ? void 0 : el.namespaceURI) === SVG_NAMESPACE;
208
208
  const isXlink = elementIsSVG && key.startsWith("xlink:");
209
209
  const isXmlns = elementIsSVG && key.startsWith("xmlns:");
210
- const isBoolean2 = shared.isSpecialBooleanAttr(key) || shared.isBooleanAttr(key);
210
+ const isBoolean = shared.isSpecialBooleanAttr(key) || shared.isBooleanAttr(key);
211
211
  if (prev === next2) {
212
212
  return;
213
213
  }
@@ -232,7 +232,7 @@ function patchAttr(el, key, prev, next2) {
232
232
  }
233
233
  return;
234
234
  }
235
- if (isBoolean2) {
235
+ if (isBoolean) {
236
236
  if (shared.includeBooleanAttr(next2)) {
237
237
  el.setAttribute(key, "");
238
238
  } else {
@@ -676,13 +676,19 @@ function insertNode(parent, child2, before) {
676
676
  }
677
677
  }
678
678
  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));
679
+ if (node instanceof Node) return node;
680
+ if (isComponent(node)) return node;
681
+ const t = typeof node;
682
+ if (node == null || t === "string" || t === "number" || t === "boolean" || t === "symbol") {
683
+ return document.createTextNode(node === false || node == null ? "" : String(node));
684
+ }
685
+ if (shared.isObject(node)) {
686
+ shared.warn(
687
+ "Rendering a plain object as a node is not recommended. The object will be converted to its string representation.",
688
+ node
689
+ );
684
690
  }
685
- return node;
691
+ return document.createTextNode(String(node));
686
692
  }
687
693
  function insert(parent, nodeFactory, before) {
688
694
  if (!parent) return;
@@ -691,7 +697,9 @@ function insert(parent, nodeFactory, before) {
691
697
  let isFirstRun = true;
692
698
  const resolveNodes = (raw) => {
693
699
  if (raw instanceof Node) return [raw];
694
- if (shared.isNull(raw) || shared.isUndefined(raw) || shared.isString(raw) || shared.isNumber(raw) || shared.isBoolean(raw)) {
700
+ if (isComponent(raw)) return [raw];
701
+ const t = typeof raw;
702
+ if (raw == null || t === "string" || t === "number" || t === "boolean") {
695
703
  return [normalizeNode(raw)];
696
704
  }
697
705
  return shared.coerceArray(raw).map((item) => shared.isFunction(item) ? item() : item).flatMap((i) => i).map(normalizeNode);
@@ -1230,154 +1238,144 @@ function addEventListener(element, event, handler, options) {
1230
1238
  }
1231
1239
 
1232
1240
  // src/binding.ts
1233
- var INPUT_CHECKBOX_CHECKED = {
1241
+ function writeValue(el, v) {
1242
+ const target = el;
1243
+ const next2 = v == null ? "" : String(v);
1244
+ if (target.value !== next2) target.value = next2;
1245
+ }
1246
+ var CHECKBOX = {
1234
1247
  event: "change",
1235
- forceChangeEvent: true,
1236
- read: (n) => n.checked,
1237
- write: (n, v) => {
1238
- const el = n;
1248
+ forceChange: true,
1249
+ read: (el) => el.checked,
1250
+ write(el, v) {
1251
+ const e = el;
1239
1252
  const next2 = Boolean(v);
1240
- if (el.checked !== next2) el.checked = next2;
1253
+ if (e.checked !== next2) e.checked = next2;
1241
1254
  }
1242
1255
  };
1243
- var INPUT_RADIO_CHECKED = {
1256
+ var RADIO = {
1244
1257
  event: "change",
1245
- forceChangeEvent: true,
1246
- read: (n) => {
1247
- const el = n;
1248
- return el.checked ? el.value : "";
1258
+ forceChange: true,
1259
+ read(el) {
1260
+ const e = el;
1261
+ return e.checked ? e.value : "";
1249
1262
  },
1250
- write: (n, v) => {
1251
- const el = n;
1252
- const next2 = String(v) === el.value;
1253
- if (el.checked !== next2) el.checked = next2;
1263
+ write(el, v) {
1264
+ const e = el;
1265
+ const next2 = String(v) === e.value;
1266
+ if (e.checked !== next2) e.checked = next2;
1254
1267
  }
1255
1268
  };
1256
- var INPUT_FILE_FILES = {
1269
+ var FILE = {
1257
1270
  event: "change",
1258
- forceChangeEvent: true,
1259
- read: (n) => n.files,
1260
- // Browsers do not allow programmatic writes to <input type="file">.
1261
- write: () => {
1271
+ forceChange: true,
1272
+ read: (el) => el.files,
1273
+ write() {
1262
1274
  }
1275
+ // browsers forbid programmatic writes to file inputs
1263
1276
  };
1264
- var INPUT_VALUE = {
1277
+ var TEXT = {
1265
1278
  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
- }
1279
+ ime: true,
1280
+ read: (el) => el.value,
1281
+ write: writeValue
1273
1282
  };
1274
- var SELECT_VALUE = {
1283
+ var TEXTAREA = {
1284
+ event: "input",
1285
+ ime: true,
1286
+ read: (el) => el.value,
1287
+ write: writeValue
1288
+ };
1289
+ var SELECT = {
1275
1290
  event: "change",
1276
- forceChangeEvent: true,
1277
- read: (n) => {
1278
- const s = n;
1291
+ forceChange: true,
1292
+ read(el) {
1293
+ const s = el;
1279
1294
  return s.multiple ? Array.from(s.selectedOptions, (o) => o.value) : s.value;
1280
1295
  },
1281
- write: (n, v) => {
1282
- const s = n;
1296
+ write(el, v) {
1297
+ const s = el;
1283
1298
  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);
1299
+ const selected = new Set((Array.isArray(v) ? v : []).map(String));
1300
+ for (const opt of Array.from(s.options)) opt.selected = selected.has(opt.value);
1286
1301
  } else {
1287
- const next2 = v == null ? "" : String(v);
1288
- if (s.value !== next2) s.value = next2;
1302
+ writeValue(el, v);
1289
1303
  }
1290
1304
  }
1291
1305
  };
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;
1306
+ function resolve(node, prop) {
1307
+ switch (node.nodeName) {
1308
+ case "INPUT":
1309
+ if (prop === "checked") return node.type === "radio" ? RADIO : CHECKBOX;
1310
+ if (prop === "files") return FILE;
1311
+ return TEXT;
1312
+ case "SELECT":
1313
+ return SELECT;
1314
+ case "TEXTAREA":
1315
+ return TEXTAREA;
1316
+ default:
1317
+ return {
1318
+ event: "input",
1319
+ read: (el) => el[prop],
1320
+ write(el, v) {
1321
+ el[prop] = v;
1322
+ }
1323
+ };
1300
1324
  }
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
1325
  }
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;
1326
+ function applyModifiers(v, trim, toNum) {
1327
+ if (!shared.isString(v)) return v;
1328
+ let s = v;
1329
+ if (trim) s = s.trim();
1330
+ if (toNum && s !== "") {
1331
+ const n = Number(s);
1332
+ if (!Number.isNaN(n)) return n;
1328
1333
  }
1329
- return val;
1334
+ return s;
1330
1335
  }
1331
- function isFocused(node) {
1332
- const root = node.getRootNode();
1333
- return (root instanceof Document || root instanceof ShadowRoot) && root.activeElement === node;
1336
+ function isFocused(el) {
1337
+ const root = el.getRootNode();
1338
+ return (root instanceof Document || root instanceof ShadowRoot) && root.activeElement === el;
1334
1339
  }
1335
1340
  function bindElement(node, prop, getter, setter, modifiers = {}) {
1336
1341
  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;
1342
+ const { event, read, write, forceChange, ime } = resolve(node, prop);
1343
+ const trim = modifiers.trim === true;
1344
+ const toNum = modifiers.number === true;
1345
+ const lazy = modifiers.lazy === true;
1346
+ const shouldCast = (trim || toNum) && prop !== "files";
1347
+ const getModel = shared.isFunction(getter) ? getter : () => getter;
1348
+ const cast = shouldCast ? (v) => applyModifiers(v, trim, toNum) : (v) => v;
1343
1349
  let composing = false;
1344
- const syncFromDom = () => {
1350
+ const eventName = lazy || forceChange ? "change" : event;
1351
+ const syncToModel = () => {
1345
1352
  if (composing) return;
1346
- const raw = strategy.read(node);
1353
+ const raw = read(node);
1347
1354
  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
- }
1355
+ const next2 = cast(raw);
1356
+ if (!Object.is(getModel(), next2)) setter(next2);
1356
1357
  };
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
- });
1358
+ addEventListener(node, eventName, syncToModel);
1359
+ if (!lazy && shouldCast && eventName !== "change") {
1360
+ addEventListener(node, "change", () => write(node, cast(read(node))));
1362
1361
  }
1363
- if (strategy.needsComposition && !lazy) {
1362
+ if (ime && !lazy) {
1364
1363
  addEventListener(node, "compositionstart", () => {
1365
1364
  composing = true;
1366
1365
  });
1367
1366
  addEventListener(node, "compositionend", () => {
1368
1367
  if (!composing) return;
1369
1368
  composing = false;
1370
- syncFromDom();
1369
+ syncToModel();
1371
1370
  });
1372
1371
  }
1373
1372
  const runner = signals.effect(() => {
1374
- const value = readModel();
1375
- if (strategy.needsComposition && !lazy && isFocused(node)) {
1373
+ const value = getModel();
1374
+ if (ime && !lazy && isFocused(node)) {
1376
1375
  if (composing) return;
1377
- const current = transform(strategy.read(node));
1378
- if (Object.is(current, value)) return;
1376
+ if (Object.is(cast(read(node)), value)) return;
1379
1377
  }
1380
- strategy.write(node, value);
1378
+ write(node, value);
1381
1379
  });
1382
1380
  if (getActiveScope()) {
1383
1381
  onCleanup(() => runner.stop());