@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.
@@ -519,6 +519,7 @@ var createDestroy = (state2, _helpers) => () => {
519
519
  }
520
520
  }
521
521
  state2.watchedFields.clear();
522
+ state2.groupWatchers.clear();
522
523
  state2.subscribers.clear();
523
524
  state2.submitHandlers.clear();
524
525
  state2.fieldCache.clear();
@@ -746,9 +747,9 @@ var writeFieldValue = (fields, value) => {
746
747
  continue;
747
748
  }
748
749
  if (field instanceof HTMLSelectElement && field.multiple) {
749
- const selectedValues = normalizedArray ?? [normalizedValue];
750
+ const selectedValues2 = normalizedArray ?? [normalizedValue];
750
751
  for (const option of field.options) {
751
- option.selected = selectedValues.includes(option.value);
752
+ option.selected = selectedValues2.includes(option.value);
752
753
  }
753
754
  continue;
754
755
  }
@@ -783,8 +784,7 @@ var submitForm = (form3) => {
783
784
  var createGetValue = (state2, helpers) => (name) => readFieldValue(helpers.getFieldsByName(name));
784
785
 
785
786
  // src/api/observe.ts
786
- var createObserve = (state2, helpers) => (field, callback) => {
787
- if (state2.isDestroyed) return () => void 0;
787
+ var observeField = (state2, field, callback) => {
788
788
  const callbacks = state2.watchedFields.get(field) ?? /* @__PURE__ */ new Set();
789
789
  callbacks.add(callback);
790
790
  state2.watchedFields.set(field, callbacks);
@@ -799,6 +799,26 @@ var createObserve = (state2, helpers) => (field, callback) => {
799
799
  }
800
800
  };
801
801
  };
802
+ var observeFields = (state2, fields, callback) => {
803
+ const names = Array.from(new Set(fields));
804
+ if (names.length === 0) return () => void 0;
805
+ const entry = { fields: names, callback };
806
+ state2.groupWatchers.add(entry);
807
+ if (state2.element) {
808
+ const values = Object.fromEntries(names.map((name) => [name, state2.api.getValue(name)]));
809
+ callback(values, null, state2.element, state2.api.getState());
810
+ }
811
+ return () => {
812
+ state2.groupWatchers.delete(entry);
813
+ };
814
+ };
815
+ var createObserve = (state2, _helpers) => {
816
+ function observe(field, callback) {
817
+ if (state2.isDestroyed) return () => void 0;
818
+ return Array.isArray(field) ? observeFields(state2, field, callback) : observeField(state2, field, callback);
819
+ }
820
+ return observe;
821
+ };
802
822
 
803
823
  // src/api/on-submit.ts
804
824
  var createOnSubmit = (state2, _helpers) => (callback, preventDefault = true) => {
@@ -870,6 +890,75 @@ var createSetErrors = (state2, helpers) => (fields) => {
870
890
  return state2.api;
871
891
  };
872
892
 
893
+ // src/api/set-options.ts
894
+ var isGroup = (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.options);
895
+ var createOption = (entry) => {
896
+ const option = document.createElement("option");
897
+ if (typeof entry === "object") {
898
+ option.value = String(entry.value);
899
+ option.textContent = entry.label ?? String(entry.value);
900
+ option.disabled = Boolean(entry.disabled);
901
+ if (entry.selected) option.defaultSelected = true;
902
+ } else {
903
+ option.value = String(entry);
904
+ option.textContent = String(entry);
905
+ }
906
+ return option;
907
+ };
908
+ var buildNodes = (options) => {
909
+ const nodes = [];
910
+ let hasExplicitSelection = false;
911
+ const track = (option) => {
912
+ if (option.defaultSelected) hasExplicitSelection = true;
913
+ return option;
914
+ };
915
+ for (const entry of options) {
916
+ if (entry === null || entry === void 0) continue;
917
+ if (isGroup(entry)) {
918
+ const group = document.createElement("optgroup");
919
+ group.label = entry.label;
920
+ group.disabled = Boolean(entry.disabled);
921
+ for (const child of entry.options) {
922
+ if (child === null || child === void 0) continue;
923
+ group.appendChild(track(createOption(child)));
924
+ }
925
+ nodes.push(group);
926
+ continue;
927
+ }
928
+ nodes.push(track(createOption(entry)));
929
+ }
930
+ return { nodes, hasExplicitSelection };
931
+ };
932
+ var selectedValues = (select) => Array.from(select.selectedOptions, (option) => option.value);
933
+ var restoreSelection = (select, previous) => {
934
+ if (previous.length === 0) return;
935
+ const options = Array.from(select.options);
936
+ if (select.multiple) {
937
+ for (const option of options) option.selected = previous.includes(option.value);
938
+ return;
939
+ }
940
+ const match = options.find((option) => option.value === previous[0]);
941
+ if (match) match.selected = true;
942
+ };
943
+ var sameValues = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
944
+ var createSetOptions = (state2, helpers) => (name, options, config = {}) => {
945
+ if (!state2.element || state2.isDestroyed) return state2.api;
946
+ const selects = helpers.getFieldsByName(name).filter((field) => field instanceof HTMLSelectElement);
947
+ if (selects.length === 0) return state2.api;
948
+ const keepValue = config.keepValue ?? true;
949
+ const changed = [];
950
+ for (const select of selects) {
951
+ const previous = selectedValues(select);
952
+ const { nodes, hasExplicitSelection } = buildNodes(options);
953
+ select.replaceChildren(...nodes);
954
+ if (keepValue && !hasExplicitSelection) restoreSelection(select, previous);
955
+ if (!sameValues(previous, selectedValues(select))) changed.push(select);
956
+ }
957
+ helpers.commitOwnMutations(selects);
958
+ changed[0]?.dispatchEvent(new Event("input", { bubbles: true }));
959
+ return state2.api;
960
+ };
961
+
873
962
  // src/api/set-value.ts
874
963
  var createSetValue = (state2, helpers) => (name, value) => {
875
964
  if (state2.isDestroyed) return state2.api;
@@ -893,28 +982,46 @@ var createSubscribe = (state2, _helpers) => (listener) => {
893
982
  };
894
983
 
895
984
  // src/api/unwatch.ts
896
- var createUnwatch = (state2, _helpers) => (field, callback) => {
897
- if (!field) {
898
- state2.watchedFields.clear();
899
- return state2.api;
900
- }
901
- if (!callback) {
902
- state2.watchedFields.delete(field);
985
+ var sameFields = (left, right) => left.length === right.size && left.every((name) => right.has(name));
986
+ var createUnwatch = (state2, _helpers) => {
987
+ function unwatch(field, callback) {
988
+ if (field === void 0 || field === "") {
989
+ state2.watchedFields.clear();
990
+ state2.groupWatchers.clear();
991
+ return state2.api;
992
+ }
993
+ if (Array.isArray(field)) {
994
+ const names = new Set(field);
995
+ for (const entry of Array.from(state2.groupWatchers)) {
996
+ if (!sameFields(entry.fields, names)) continue;
997
+ if (callback && entry.callback !== callback) continue;
998
+ state2.groupWatchers.delete(entry);
999
+ }
1000
+ return state2.api;
1001
+ }
1002
+ if (!callback) {
1003
+ state2.watchedFields.delete(field);
1004
+ return state2.api;
1005
+ }
1006
+ const callbacks = state2.watchedFields.get(field);
1007
+ callbacks?.delete(callback);
1008
+ if (callbacks && callbacks.size === 0) {
1009
+ state2.watchedFields.delete(field);
1010
+ }
903
1011
  return state2.api;
904
1012
  }
905
- const callbacks = state2.watchedFields.get(field);
906
- callbacks?.delete(callback);
907
- if (callbacks && callbacks.size === 0) {
908
- state2.watchedFields.delete(field);
909
- }
910
- return state2.api;
1013
+ return unwatch;
911
1014
  };
912
1015
 
913
1016
  // src/api/watch.ts
914
- var createWatch = (state2, helpers) => (field, callback) => {
1017
+ var createWatch = (state2, helpers) => {
915
1018
  const observe = createObserve(state2, helpers);
916
- observe(field, callback);
917
- return state2.api;
1019
+ function watch(field, callback) {
1020
+ if (Array.isArray(field)) observe(field, callback);
1021
+ else observe(field, callback);
1022
+ return state2.api;
1023
+ }
1024
+ return watch;
918
1025
  };
919
1026
 
920
1027
  // src/core/validation.ts
@@ -1067,6 +1174,7 @@ var createFormController = (target, options = {}) => {
1067
1174
  attributes,
1068
1175
  validators: normalizedOptions.validators ?? {},
1069
1176
  watchedFields: /* @__PURE__ */ new Map(),
1177
+ groupWatchers: /* @__PURE__ */ new Set(),
1070
1178
  subscribers: /* @__PURE__ */ new Set(),
1071
1179
  submitHandlers: /* @__PURE__ */ new Set(),
1072
1180
  fieldCache: /* @__PURE__ */ new Map(),
@@ -1237,7 +1345,7 @@ var createFormController = (target, options = {}) => {
1237
1345
  }
1238
1346
  return affected;
1239
1347
  };
1240
- const emitFieldWatchers = (name) => {
1348
+ const emitSingleWatchers = (name) => {
1241
1349
  if (!state2.element || !state2.api) return;
1242
1350
  const callbacks = state2.watchedFields.get(name);
1243
1351
  if (!callbacks || callbacks.size === 0) return;
@@ -1246,6 +1354,23 @@ var createFormController = (target, options = {}) => {
1246
1354
  const snapshot = state2.api.getState();
1247
1355
  callbacks.forEach((callback) => callback(value, fieldElement, state2.element, snapshot));
1248
1356
  };
1357
+ const emitGroupWatchers = (names) => {
1358
+ if (!state2.element || !state2.api || state2.groupWatchers.size === 0) return;
1359
+ let snapshot;
1360
+ for (const entry of Array.from(state2.groupWatchers)) {
1361
+ const changed = names ? names.find((name) => entry.fields.includes(name)) : null;
1362
+ if (changed === void 0) continue;
1363
+ snapshot ?? (snapshot = state2.api.getState());
1364
+ const values = Object.fromEntries(
1365
+ entry.fields.map((field) => [field, state2.api.getValue(field)])
1366
+ );
1367
+ entry.callback(values, changed, state2.element, snapshot);
1368
+ }
1369
+ };
1370
+ const emitFieldWatchers = (name) => {
1371
+ emitSingleWatchers(name);
1372
+ emitGroupWatchers([name]);
1373
+ };
1249
1374
  const runAutoSubmit = () => {
1250
1375
  state2.autoSubmitTimer = null;
1251
1376
  if (!state2.element || state2.isDestroyed || !state2.autoSubmitEnabled) return;
@@ -1266,8 +1391,20 @@ var createFormController = (target, options = {}) => {
1266
1391
  }
1267
1392
  runAutoSubmit();
1268
1393
  };
1394
+ const selectSnapshots = /* @__PURE__ */ new WeakMap();
1395
+ const readSelectSnapshot = (select) => Array.from(select.selectedOptions, (option) => option.value).join("\0");
1396
+ const snapshotSelect = (select) => {
1397
+ selectSnapshots.set(select, readSelectSnapshot(select));
1398
+ };
1399
+ const snapshotAllSelects = () => {
1400
+ if (!state2.element) return;
1401
+ for (const field of getNamedFields(state2.element)) {
1402
+ if (field instanceof HTMLSelectElement) snapshotSelect(field);
1403
+ }
1404
+ };
1269
1405
  const emitAllFieldWatchers = () => {
1270
- for (const name of Array.from(state2.watchedFields.keys())) emitFieldWatchers(name);
1406
+ for (const name of Array.from(state2.watchedFields.keys())) emitSingleWatchers(name);
1407
+ emitGroupWatchers(null);
1271
1408
  };
1272
1409
  const applyReset = () => {
1273
1410
  if (!state2.element || state2.isDestroyed) return;
@@ -1279,6 +1416,7 @@ var createFormController = (target, options = {}) => {
1279
1416
  state2.validationElementErrors.clear();
1280
1417
  clearAttributes(state2.element, [attributes.error, attributes.filled, "aria-invalid"]);
1281
1418
  syncVisualState();
1419
+ snapshotAllSelects();
1282
1420
  emitAllFieldWatchers();
1283
1421
  notifySubscribers();
1284
1422
  };
@@ -1293,7 +1431,8 @@ var createFormController = (target, options = {}) => {
1293
1431
  validateNames,
1294
1432
  emitFieldWatchers,
1295
1433
  scheduleAutoSubmit,
1296
- applyReset
1434
+ applyReset,
1435
+ commitOwnMutations: (selects) => commitOwnMutations(selects)
1297
1436
  };
1298
1437
  const handleDelegatedEvent = (event) => {
1299
1438
  const target2 = event.target;
@@ -1301,6 +1440,7 @@ var createFormController = (target, options = {}) => {
1301
1440
  const name = target2.name;
1302
1441
  if (!name) return;
1303
1442
  clearFieldCache();
1443
+ if (target2 instanceof HTMLSelectElement) snapshotSelect(target2);
1304
1444
  const canonical = resolveCanonicalForName(state2, name) ?? name;
1305
1445
  const displayName = resolveDisplayNameForName(state2, name);
1306
1446
  if (normalizedOptions.clearManualErrorsOnChange) {
@@ -1311,8 +1451,9 @@ var createFormController = (target, options = {}) => {
1311
1451
  if (state2.isValidated && affectedValidationNames.some((field) => state2.validators[field] !== void 0)) {
1312
1452
  validateNames(affectedValidationNames);
1313
1453
  }
1314
- emitFieldWatchers(canonical);
1315
- if (canonical !== name) emitFieldWatchers(name);
1454
+ emitSingleWatchers(canonical);
1455
+ if (canonical !== name) emitSingleWatchers(name);
1456
+ emitGroupWatchers(canonical !== name ? [canonical, name] : [canonical]);
1316
1457
  notifySubscribers();
1317
1458
  scheduleAutoSubmit();
1318
1459
  };
@@ -1393,18 +1534,62 @@ var createFormController = (target, options = {}) => {
1393
1534
  const cancel = () => clearTimeout(timer);
1394
1535
  state2.cleanups.add(cancel);
1395
1536
  };
1537
+ const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1538
+ const isStructural = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1539
+ const isOptionNode = (node) => node instanceof HTMLOptionElement || node instanceof HTMLOptGroupElement;
1540
+ const optionListOwner = (record) => {
1541
+ if (record.type !== "childList") return null;
1542
+ const touchesOptions = Array.from(record.addedNodes).some(isOptionNode) || Array.from(record.removedNodes).some(isOptionNode);
1543
+ if (!touchesOptions) return null;
1544
+ const target2 = record.target;
1545
+ if (target2 instanceof HTMLSelectElement) return target2;
1546
+ if (target2 instanceof HTMLOptGroupElement && target2.parentElement instanceof HTMLSelectElement) {
1547
+ return target2.parentElement;
1548
+ }
1549
+ return null;
1550
+ };
1551
+ const handleMutations = (records) => {
1552
+ if (state2.isDestroyed) return;
1553
+ const structural = records.some(isStructural);
1554
+ const changedNames = /* @__PURE__ */ new Set();
1555
+ for (const record of records) {
1556
+ const select = optionListOwner(record);
1557
+ if (!select || !select.name) continue;
1558
+ const next = readSelectSnapshot(select);
1559
+ if (selectSnapshots.get(select) !== next) changedNames.add(select.name);
1560
+ selectSnapshots.set(select, next);
1561
+ }
1562
+ if (!structural && changedNames.size === 0) return;
1563
+ clearFieldCache();
1564
+ if (structural) {
1565
+ snapshotAllSelects();
1566
+ if (state2.isValidated) validateNames();
1567
+ else syncVisualState();
1568
+ } else {
1569
+ const affected = /* @__PURE__ */ new Set();
1570
+ for (const name of changedNames) {
1571
+ for (const field of getAffectedValidationNames(name)) affected.add(field);
1572
+ }
1573
+ const names = Array.from(affected);
1574
+ syncVisualState(Array.from(changedNames));
1575
+ if (state2.isValidated && names.some((field) => state2.validators[field] !== void 0)) {
1576
+ validateNames(names);
1577
+ }
1578
+ }
1579
+ for (const name of changedNames) emitFieldWatchers(name);
1580
+ notifySubscribers();
1581
+ };
1582
+ const commitOwnMutations = (selects) => {
1583
+ selects.forEach(snapshotSelect);
1584
+ const pending = state2.mutationObserver?.takeRecords() ?? [];
1585
+ const foreign = pending.filter(
1586
+ (record) => !selects.some((select) => select.contains(record.target))
1587
+ );
1588
+ if (foreign.length > 0) handleMutations(foreign);
1589
+ };
1396
1590
  const startMutationObserver = () => {
1397
1591
  if (!state2.element || typeof MutationObserver === "undefined") return;
1398
- const touchesFields = (node) => node instanceof Element && (isFieldElement(node) || node.querySelector("input, select, textarea") !== null);
1399
- const isRelevant = (record) => record.type === "attributes" || Array.from(record.addedNodes).some(touchesFields) || Array.from(record.removedNodes).some(touchesFields);
1400
- state2.mutationObserver = new MutationObserver((records) => {
1401
- if (state2.isDestroyed || !records.some(isRelevant)) return;
1402
- clearFieldCache();
1403
- if (state2.isValidated) {
1404
- validateNames();
1405
- } else syncVisualState();
1406
- notifySubscribers();
1407
- });
1592
+ state2.mutationObserver = new MutationObserver(handleMutations);
1408
1593
  state2.mutationObserver.observe(state2.element, {
1409
1594
  childList: true,
1410
1595
  subtree: true,
@@ -1433,6 +1618,7 @@ var createFormController = (target, options = {}) => {
1433
1618
  setErrors: createSetErrors(state2, helpers),
1434
1619
  clearErrors: createClearErrors(state2, helpers),
1435
1620
  setValue: createSetValue(state2, helpers),
1621
+ setOptions: createSetOptions(state2, helpers),
1436
1622
  getValue: createGetValue(state2, helpers),
1437
1623
  getField: createGetField(state2, helpers),
1438
1624
  validate: createValidate(state2, helpers),
@@ -1452,6 +1638,7 @@ var createFormController = (target, options = {}) => {
1452
1638
  addListener2(document, "input", handleExternalDelegatedEvent);
1453
1639
  addListener2(state2.element, "submit", handleSubmitEvent);
1454
1640
  addListener2(state2.element, "reset", handleResetEvent);
1641
+ snapshotAllSelects();
1455
1642
  startMutationObserver();
1456
1643
  }
1457
1644
  if (normalizedOptions.autoSubmit) {