@samline/forms 2.8.0 → 2.9.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.
package/dist/index.cjs CHANGED
@@ -520,6 +520,7 @@ var createDestroy = (state2, _helpers) => () => {
520
520
  }
521
521
  }
522
522
  state2.watchedFields.clear();
523
+ state2.groupWatchers.clear();
523
524
  state2.subscribers.clear();
524
525
  state2.submitHandlers.clear();
525
526
  state2.fieldCache.clear();
@@ -747,9 +748,9 @@ var writeFieldValue = (fields, value) => {
747
748
  continue;
748
749
  }
749
750
  if (field instanceof HTMLSelectElement && field.multiple) {
750
- const selectedValues = normalizedArray ?? [normalizedValue];
751
+ const selectedValues2 = normalizedArray ?? [normalizedValue];
751
752
  for (const option of field.options) {
752
- option.selected = selectedValues.includes(option.value);
753
+ option.selected = selectedValues2.includes(option.value);
753
754
  }
754
755
  continue;
755
756
  }
@@ -784,8 +785,7 @@ var submitForm = (form2) => {
784
785
  var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFieldsByName(name));
785
786
 
786
787
  // src/api/observe.ts
787
- var createObserve = (state2, helpers) => (field, callback) => {
788
- if (state2.isDestroyed) return () => void 0;
788
+ var observeField = (state2, field, callback) => {
789
789
  const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
790
790
  callbacks.add(callback);
791
791
  state2.watchedFields.set(field, callbacks);
@@ -800,6 +800,26 @@ var createObserve = (state2, helpers) => (field, callback) => {
800
800
  }
801
801
  };
802
802
  };
803
+ var observeFields = (state2, fields, callback) => {
804
+ const names = Array.from(new Set(fields));
805
+ if (names.length === 0) return () => void 0;
806
+ const entry = { fields: names, callback };
807
+ state2.groupWatchers.add(entry);
808
+ if (state2.element) {
809
+ const values = Object.fromEntries(names.map((name) => [name, state2.api.getValue(name)]));
810
+ callback(values, null, state2.element, state2.api.getState());
811
+ }
812
+ return () => {
813
+ state2.groupWatchers.delete(entry);
814
+ };
815
+ };
816
+ var createObserve = (state2, _helpers) => {
817
+ function observe(field, callback) {
818
+ if (state2.isDestroyed) return () => void 0;
819
+ return Array.isArray(field) ? observeFields(state2, field, callback) : observeField(state2, field, callback);
820
+ }
821
+ return observe;
822
+ };
803
823
 
804
824
  // src/api/on-submit.ts
805
825
  var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
@@ -871,6 +891,75 @@ var createSetErrors = (state2, helpers) => (fields) => {
871
891
  return state2.api;
872
892
  };
873
893
 
894
+ // src/api/set-options.ts
895
+ var isGroup = (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.options);
896
+ var createOption = (entry) => {
897
+ const option = document.createElement("option");
898
+ if (typeof entry === "object") {
899
+ option.value = String(entry.value);
900
+ option.textContent = entry.label ?? String(entry.value);
901
+ option.disabled = Boolean(entry.disabled);
902
+ if (entry.selected) option.defaultSelected = true;
903
+ } else {
904
+ option.value = String(entry);
905
+ option.textContent = String(entry);
906
+ }
907
+ return option;
908
+ };
909
+ var buildNodes = (options) => {
910
+ const nodes = [];
911
+ let hasExplicitSelection = false;
912
+ const track = (option) => {
913
+ if (option.defaultSelected) hasExplicitSelection = true;
914
+ return option;
915
+ };
916
+ for (const entry of options) {
917
+ if (entry === null || entry === void 0) continue;
918
+ if (isGroup(entry)) {
919
+ const group = document.createElement("optgroup");
920
+ group.label = entry.label;
921
+ group.disabled = Boolean(entry.disabled);
922
+ for (const child of entry.options) {
923
+ if (child === null || child === void 0) continue;
924
+ group.appendChild(track(createOption(child)));
925
+ }
926
+ nodes.push(group);
927
+ continue;
928
+ }
929
+ nodes.push(track(createOption(entry)));
930
+ }
931
+ return { nodes, hasExplicitSelection };
932
+ };
933
+ var selectedValues = (select) => Array.from(select.selectedOptions, (option) => option.value);
934
+ var restoreSelection = (select, previous) => {
935
+ if (previous.length === 0) return;
936
+ const options = Array.from(select.options);
937
+ if (select.multiple) {
938
+ for (const option of options) option.selected = previous.includes(option.value);
939
+ return;
940
+ }
941
+ const match = options.find((option) => option.value === previous[0]);
942
+ if (match) match.selected = true;
943
+ };
944
+ var sameValues = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
945
+ var createSetOptions = (state2, helpers) => (name, options, config = {}) => {
946
+ if (!state2.element || state2.isDestroyed) return state2.api;
947
+ const selects = helpers.getFieldsByName(name).filter((field) => field instanceof HTMLSelectElement);
948
+ if (selects.length === 0) return state2.api;
949
+ const keepValue = config.keepValue ?? true;
950
+ const changed = [];
951
+ for (const select of selects) {
952
+ const previous = selectedValues(select);
953
+ const { nodes, hasExplicitSelection } = buildNodes(options);
954
+ select.replaceChildren(...nodes);
955
+ if (keepValue && !hasExplicitSelection) restoreSelection(select, previous);
956
+ if (!sameValues(previous, selectedValues(select))) changed.push(select);
957
+ }
958
+ helpers.commitOwnMutations(selects);
959
+ changed[0]?.dispatchEvent(new Event("input", { bubbles: true }));
960
+ return state2.api;
961
+ };
962
+
874
963
  // src/api/set-value.ts
875
964
  var createSetValue = (state2, helpers) => (name, value) => {
876
965
  if (state2.isDestroyed) return state2.api;
@@ -894,28 +983,46 @@ var createSubscribe = (state2, _helpers) => (listener) => {
894
983
  };
895
984
 
896
985
  // src/api/unwatch.ts
897
- var createUnwatch = (state2, _helpers) => (field, callback) => {
898
- if (!field) {
899
- state2.watchedFields.clear();
900
- return state2.api;
901
- }
902
- if (!callback) {
903
- state2.watchedFields.delete(field);
986
+ var sameFields = (left, right) => left.length === right.size && left.every((name) => right.has(name));
987
+ var createUnwatch = (state2, _helpers) => {
988
+ function unwatch(field, callback) {
989
+ if (field === void 0 || field === "") {
990
+ state2.watchedFields.clear();
991
+ state2.groupWatchers.clear();
992
+ return state2.api;
993
+ }
994
+ if (Array.isArray(field)) {
995
+ const names = new Set(field);
996
+ for (const entry of Array.from(state2.groupWatchers)) {
997
+ if (!sameFields(entry.fields, names)) continue;
998
+ if (callback && entry.callback !== callback) continue;
999
+ state2.groupWatchers.delete(entry);
1000
+ }
1001
+ return state2.api;
1002
+ }
1003
+ if (!callback) {
1004
+ state2.watchedFields.delete(field);
1005
+ return state2.api;
1006
+ }
1007
+ const callbacks = state2.watchedFields.get(field);
1008
+ callbacks?.delete(callback);
1009
+ if (callbacks && callbacks.size === 0) {
1010
+ state2.watchedFields.delete(field);
1011
+ }
904
1012
  return state2.api;
905
1013
  }
906
- const callbacks = state2.watchedFields.get(field);
907
- callbacks?.delete(callback);
908
- if (callbacks && callbacks.size === 0) {
909
- state2.watchedFields.delete(field);
910
- }
911
- return state2.api;
1014
+ return unwatch;
912
1015
  };
913
1016
 
914
1017
  // src/api/watch.ts
915
- var createWatch = (state2, helpers) => (field, callback) => {
1018
+ var createWatch = (state2, helpers) => {
916
1019
  const observe = createObserve(state2, helpers);
917
- observe(field, callback);
918
- return state2.api;
1020
+ function watch(field, callback) {
1021
+ if (Array.isArray(field)) observe(field, callback);
1022
+ else observe(field, callback);
1023
+ return state2.api;
1024
+ }
1025
+ return watch;
919
1026
  };
920
1027
 
921
1028
  // src/core/validation.ts
@@ -1083,6 +1190,7 @@ var createFormController = (target, options = {}) => {
1083
1190
  attributes,
1084
1191
  validators: normalizedOptions.validators ?? {},
1085
1192
  watchedFields: /* @__PURE__ */ new Map(),
1193
+ groupWatchers: /* @__PURE__ */ new Set(),
1086
1194
  subscribers: /* @__PURE__ */ new Set(),
1087
1195
  submitHandlers: /* @__PURE__ */ new Set(),
1088
1196
  fieldCache: /* @__PURE__ */ new Map(),
@@ -1253,7 +1361,7 @@ var createFormController = (target, options = {}) => {
1253
1361
  }
1254
1362
  return affected;
1255
1363
  };
1256
- const emitFieldWatchers = (name) => {
1364
+ const emitSingleWatchers = (name) => {
1257
1365
  if (!state2.element || !state2.api) return;
1258
1366
  const callbacks = state2.watchedFields.get(name);
1259
1367
  if (!callbacks || callbacks.size === 0) return;
@@ -1262,6 +1370,23 @@ var createFormController = (target, options = {}) => {
1262
1370
  const snapshot = state2.api.getState();
1263
1371
  callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
1264
1372
  };
1373
+ const emitGroupWatchers = (names) => {
1374
+ if (!state2.element || !state2.api || state2.groupWatchers.size === 0) return;
1375
+ let snapshot;
1376
+ for (const entry of Array.from(state2.groupWatchers)) {
1377
+ const changed = names ? names.find((name) => entry.fields.includes(name)) : null;
1378
+ if (changed === void 0) continue;
1379
+ snapshot ?? (snapshot = state2.api.getState());
1380
+ const values = Object.fromEntries(
1381
+ entry.fields.map((field) => [field, state2.api.getValue(field)])
1382
+ );
1383
+ entry.callback(values, changed, state2.element, snapshot);
1384
+ }
1385
+ };
1386
+ const emitFieldWatchers = (name) => {
1387
+ emitSingleWatchers(name);
1388
+ emitGroupWatchers([name]);
1389
+ };
1265
1390
  const runAutoSubmit = () => {
1266
1391
  state2.autoSubmitTimer = null;
1267
1392
  if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
@@ -1282,8 +1407,20 @@ var createFormController = (target, options = {}) => {
1282
1407
  }
1283
1408
  runAutoSubmit();
1284
1409
  };
1410
+ const selectSnapshots = /* @__PURE__ */ new WeakMap();
1411
+ const readSelectSnapshot = (select) => Array.from(select.selectedOptions, (option) => option.value).join("\0");
1412
+ const snapshotSelect = (select) => {
1413
+ selectSnapshots.set(select, readSelectSnapshot(select));
1414
+ };
1415
+ const snapshotAllSelects = () => {
1416
+ if (!state2.element) return;
1417
+ for (const field of getNamedFields(state2.element)) {
1418
+ if (field instanceof HTMLSelectElement) snapshotSelect(field);
1419
+ }
1420
+ };
1285
1421
  const emitAllFieldWatchers = () => {
1286
- for (const name of Array.from(state2.watchedFields.keys())) emitFieldWatchers(name);
1422
+ for (const name of Array.from(state2.watchedFields.keys())) emitSingleWatchers(name);
1423
+ emitGroupWatchers(null);
1287
1424
  };
1288
1425
  const applyReset = () => {
1289
1426
  if (!state2.element || state2.isDestroyed) return;
@@ -1295,6 +1432,7 @@ var createFormController = (target, options = {}) => {
1295
1432
  state2.validationElementErrors.clear();
1296
1433
  clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
1297
1434
  syncVisualState();
1435
+ snapshotAllSelects();
1298
1436
  emitAllFieldWatchers();
1299
1437
  notifySubscribers();
1300
1438
  };
@@ -1309,7 +1447,8 @@ var createFormController = (target, options = {}) => {
1309
1447
  validateNames,
1310
1448
  emitFieldWatchers,
1311
1449
  scheduleAutoSubmit,
1312
- applyReset
1450
+ applyReset,
1451
+ commitOwnMutations: (selects) => commitOwnMutations(selects)
1313
1452
  };
1314
1453
  const handleDelegatedEvent = (event) => {
1315
1454
  const target2 = event.target;
@@ -1317,6 +1456,7 @@ var createFormController = (target, options = {}) => {
1317
1456
  const name = target2.name;
1318
1457
  if (!name) return;
1319
1458
  clearFieldCache();
1459
+ if (target2 instanceof HTMLSelectElement) snapshotSelect(target2);
1320
1460
  const canonical = resolveCanonicalForName(state2, name) ?? name;
1321
1461
  const displayName = resolveDisplayNameForName(state2, name);
1322
1462
  if (normalizedOptions.clearManualErrorsOnChange) {
@@ -1327,8 +1467,9 @@ var createFormController = (target, options = {}) => {
1327
1467
  if (state2.isValidated && affectedValidationNames.some((field) => state2.validators[field] !== void 0)) {
1328
1468
  validateNames(affectedValidationNames);
1329
1469
  }
1330
- emitFieldWatchers(canonical);
1331
- if (canonical !== name) emitFieldWatchers(name);
1470
+ emitSingleWatchers(canonical);
1471
+ if (canonical !== name) emitSingleWatchers(name);
1472
+ emitGroupWatchers(canonical !== name ? [canonical, name] : [canonical]);
1332
1473
  notifySubscribers();
1333
1474
  scheduleAutoSubmit();
1334
1475
  };
@@ -1409,18 +1550,62 @@ var createFormController = (target, options = {}) => {
1409
1550
  const cancel = () => clearTimeout(timer);
1410
1551
  state2.cleanups.add(cancel);
1411
1552
  };
1553
+ const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1554
+ const isStructural = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1555
+ const isOptionNode = (node) => node instanceof HTMLOptionElement || node instanceof HTMLOptGroupElement;
1556
+ const optionListOwner = (record) => {
1557
+ if (record.type !== "childList") return null;
1558
+ const touchesOptions = Array.from(record.addedNodes).some(isOptionNode) || Array.from(record.removedNodes).some(isOptionNode);
1559
+ if (!touchesOptions) return null;
1560
+ const target2 = record.target;
1561
+ if (target2 instanceof HTMLSelectElement) return target2;
1562
+ if (target2 instanceof HTMLOptGroupElement && target2.parentElement instanceof HTMLSelectElement) {
1563
+ return target2.parentElement;
1564
+ }
1565
+ return null;
1566
+ };
1567
+ const handleMutations = (records) => {
1568
+ if (state2.isDestroyed) return;
1569
+ const structural = records.some(isStructural);
1570
+ const changedNames = /* @__PURE__ */ new Set();
1571
+ for (const record of records) {
1572
+ const select = optionListOwner(record);
1573
+ if (!select || !select.name) continue;
1574
+ const next = readSelectSnapshot(select);
1575
+ if (selectSnapshots.get(select) !== next) changedNames.add(select.name);
1576
+ selectSnapshots.set(select, next);
1577
+ }
1578
+ if (!structural && changedNames.size === 0) return;
1579
+ clearFieldCache();
1580
+ if (structural) {
1581
+ snapshotAllSelects();
1582
+ if (state2.isValidated) validateNames();
1583
+ else syncVisualState();
1584
+ } else {
1585
+ const affected = /* @__PURE__ */ new Set();
1586
+ for (const name of changedNames) {
1587
+ for (const field of getAffectedValidationNames(name)) affected.add(field);
1588
+ }
1589
+ const names = Array.from(affected);
1590
+ syncVisualState(Array.from(changedNames));
1591
+ if (state2.isValidated && names.some((field) => state2.validators[field] !== void 0)) {
1592
+ validateNames(names);
1593
+ }
1594
+ }
1595
+ for (const name of changedNames) emitFieldWatchers(name);
1596
+ notifySubscribers();
1597
+ };
1598
+ const commitOwnMutations = (selects) => {
1599
+ selects.forEach(snapshotSelect);
1600
+ const pending = state2.mutationObserver?.takeRecords() ?? [];
1601
+ const foreign = pending.filter(
1602
+ (record) => !selects.some((select) => select.contains(record.target))
1603
+ );
1604
+ if (foreign.length > 0) handleMutations(foreign);
1605
+ };
1412
1606
  const startMutationObserver = () => {
1413
1607
  if (!state2.element || typeof MutationObserver === "undefined") return;
1414
- const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1415
- const isRelevant = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1416
- state2.mutationObserver = new MutationObserver((records) => {
1417
- if (state2.isDestroyed || !records.some(isRelevant)) return;
1418
- clearFieldCache();
1419
- if (state2.isValidated) {
1420
- validateNames();
1421
- } else syncVisualState();
1422
- notifySubscribers();
1423
- });
1608
+ state2.mutationObserver = new MutationObserver(handleMutations);
1424
1609
  state2.mutationObserver.observe(state2.element, {
1425
1610
  childList: true,
1426
1611
  subtree: true,
@@ -1449,6 +1634,7 @@ var createFormController = (target, options = {}) => {
1449
1634
  setErrors: createSetErrors(state2, helpers),
1450
1635
  clearErrors: createClearErrors(state2, helpers),
1451
1636
  setValue: createSetValue(state2, helpers),
1637
+ setOptions: createSetOptions(state2, helpers),
1452
1638
  getValue: createGetValue(state2, helpers),
1453
1639
  getField: createGetField(state2, helpers),
1454
1640
  validate: createValidate(state2, helpers),
@@ -1468,6 +1654,7 @@ var createFormController = (target, options = {}) => {
1468
1654
  addListener2(document, "input", handleExternalDelegatedEvent);
1469
1655
  addListener2(state2.element, "submit", handleSubmitEvent);
1470
1656
  addListener2(state2.element, "reset", handleResetEvent);
1657
+ snapshotAllSelects();
1471
1658
  startMutationObserver();
1472
1659
  }
1473
1660
  if (normalizedOptions.autoSubmit) {