mobx-formkit 1.1.2 → 1.1.4

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.
@@ -129,6 +129,8 @@
129
129
  class ArrayMap {
130
130
  /** Duck-typing marker for utility code */
131
131
  _isArrayMap = true;
132
+ /** Called after a successful `move()` — used to flag the change as dirty */
133
+ onMove;
132
134
  /** Internal observable array: entries stored as [key, value] pairs */
133
135
  _entries = mobx.observable.array([]);
134
136
  constructor(entries) {
@@ -246,6 +248,7 @@
246
248
  return;
247
249
  const [entry] = this._entries.splice(fromIndex, 1);
248
250
  this._entries.splice(toIndex, 0, entry);
251
+ this.onMove?.();
249
252
  });
250
253
  }
251
254
  /**
@@ -963,7 +966,14 @@
963
966
  set: mobx.action,
964
967
  add: mobx.action,
965
968
  del: mobx.action,
969
+ move: mobx.action,
966
970
  });
971
+ // Any reorder of the fields (also when `fields.move()` is called
972
+ // directly) flags the field and the form as changed. (#657)
973
+ this.fields.onMove = () => {
974
+ this.$changed++;
975
+ this.state.form.$changed++;
976
+ };
967
977
  }
968
978
  execHook = (name, fallback = {}, ...args) => $try(fallback[name], this.$hooks[name], this.noop).apply(this, [this, ...args]);
969
979
  execHandler = (name, args, fallback = undefined, hook = null, execHook = true) => [
@@ -1085,7 +1095,7 @@
1085
1095
  }
1086
1096
  });
1087
1097
  }
1088
- initField(key, path, data, update = false) {
1098
+ initField(key, path, data, update = false, value) {
1089
1099
  const initial = this.state.get("current", "props");
1090
1100
  const struct = pathToStruct(path);
1091
1101
  // try to get props from separated objects
@@ -1101,7 +1111,9 @@
1101
1111
  return t;
1102
1112
  };
1103
1113
  const props = {
1104
- $value: lodash.get(initial[SeparatedPropsMode.values], path),
1114
+ $value: value !== void 0
1115
+ ? value
1116
+ : lodash.get(initial[SeparatedPropsMode.values], path),
1105
1117
  $computed: _try(SeparatedPropsMode.computed),
1106
1118
  $label: _try(SeparatedPropsMode.labels),
1107
1119
  $placeholder: _try(SeparatedPropsMode.placeholders),
@@ -1204,10 +1216,21 @@
1204
1216
  deepCheck(type, prop, fields) {
1205
1217
  const $fields = getObservableMapValues(fields);
1206
1218
  return lodash.transform($fields, (check, field) => {
1207
- if (!field.fields.size ||
1208
- !Array.isArray(field.initial) ||
1209
- prop === FieldPropsEnum.isValid ||
1210
- prop === FieldPropsEnum.hasError) {
1219
+ // Array containers are skipped because most of their props are
1220
+ // computed recursively on their nested fields. Some props are not
1221
+ // recursive (isDirty/isPristine compare the container value with
1222
+ // its initial value) or must aggregate the container itself
1223
+ // (isValid/hasError), so they are taken into account for array
1224
+ // containers too. (#656, #657)
1225
+ const isContainerProp = [
1226
+ FieldPropsEnum.isValid,
1227
+ FieldPropsEnum.hasError,
1228
+ FieldPropsEnum.isDirty,
1229
+ FieldPropsEnum.isPristine,
1230
+ ].includes(prop);
1231
+ if (isContainerProp ||
1232
+ !field.fields.size ||
1233
+ !Array.isArray(field.initial)) {
1211
1234
  check.push(field[prop]);
1212
1235
  }
1213
1236
  check.push(checkPropOccurrence({
@@ -1251,6 +1274,17 @@
1251
1274
  this.state.form.select(this.path ?? "", null, false);
1252
1275
  const applyInputConverterOnUpdate = this.state.options.get(OptionsEnum.applyInputConverterOnUpdate, this);
1253
1276
  if ($field != null && field !== void 0) {
1277
+ // an array provided for a field which is not an array container
1278
+ // is the field value itself (not a set of nested fields)
1279
+ const $rawValue = lodash.get(raw, $path);
1280
+ const $absPath = [this.path ?? "", $path].join(".").replace(/^\.+/, "");
1281
+ if (Array.isArray($rawValue) && !this.isArrayContainer($absPath)) {
1282
+ $field.value = parseInput(applyInputConverterOnUpdate ? $field.$input : (val) => val, {
1283
+ fallbackValueOption: this.state.options.get(OptionsEnum.fallbackValue, this),
1284
+ separated: $rawValue,
1285
+ });
1286
+ return;
1287
+ }
1254
1288
  if (Array.isArray($field.values())) {
1255
1289
  const n = Math.max(-1, ...Object.keys(field.fields || {}).map(Number));
1256
1290
  getObservableMapValues($field.fields).forEach(($f) => {
@@ -1288,17 +1322,18 @@
1288
1322
  // init field into the container field
1289
1323
  $container.$changed++;
1290
1324
  $container.state.form.$changed++;
1291
- $container.initField($key, $newFieldPath, field, true);
1325
+ // the value provided by the update wins over the declared values
1326
+ $container.initField($key, $newFieldPath, field, true, lodash.get(raw, $path));
1292
1327
  }
1293
1328
  else if (recursion) {
1294
1329
  if (lodash.has(field, FieldPropsEnum.fields) && field.fields != null) {
1295
1330
  // handle nested fields if defined
1296
- this.deepUpdate(field.fields, $path);
1331
+ this.deepUpdate(field.fields, $path, true, raw);
1297
1332
  }
1298
1333
  else {
1299
1334
  // handle nested fields if undefined or null
1300
1335
  const $fields = pathToFieldsTree(this.state.struct(), $path);
1301
- this.deepUpdate($fields, $path, false);
1336
+ this.deepUpdate($fields, $path, false, raw);
1302
1337
  }
1303
1338
  }
1304
1339
  });
@@ -1473,18 +1508,71 @@
1473
1508
  execEvent && this.execHook(FieldPropsEnum.onAdd);
1474
1509
  return this;
1475
1510
  }
1476
- let key;
1477
- if (lodash.has(obj, FieldPropsEnum.key))
1478
- key = obj.key;
1479
- if (lodash.has(obj, FieldPropsEnum.name))
1480
- key = obj.name;
1481
- if (!key)
1482
- key = maxKey(this.fields);
1483
- const $path = ($key) => [this.path, $key].join(".").replace(/^\.+/, "");
1511
+ // a plain field holding an array value gets the new item appended
1512
+ if (this.isPlainArrayField() &&
1513
+ !lodash.has(obj, FieldPropsEnum.key) &&
1514
+ !lodash.has(obj, FieldPropsEnum.name)) {
1515
+ const $values = Array.isArray(this.value)
1516
+ ? [...this.value]
1517
+ : [];
1518
+ $values.push(lodash.has(obj, FieldPropsEnum.value)
1519
+ ? obj[FieldPropsEnum.value]
1520
+ : defaultValue({
1521
+ fallbackValueOption: this.state.options.get(OptionsEnum.fallbackValue, this),
1522
+ nullable: this.$nullable,
1523
+ type: this.type,
1524
+ }));
1525
+ this.set(FieldPropsEnum.value, $values);
1526
+ this.$changed++;
1527
+ this.state.form.$changed++;
1528
+ execEvent && this.execHook(FieldPropsEnum.onAdd);
1529
+ return this;
1530
+ }
1531
+ const field = this.createField(obj);
1532
+ this.$changed++;
1533
+ this.state.form.$changed++;
1534
+ execEvent && this.execHook(FieldPropsEnum.onAdd);
1535
+ return field;
1536
+ }
1537
+ /**
1538
+ Create a new field into this container (no change counters, no hooks)
1539
+ */
1540
+ createField(obj, key) {
1484
1541
  const tree = pathToFieldsTree(this.state.struct(), this.path ?? "", 0, true);
1485
- const field = this.initField(key, $path(key), lodash.merge(tree[0], obj));
1542
+ const nestedFields = tree[0]?.fields ?? null;
1543
+ // An object without field-definition props, whose keys match the
1544
+ // nested fields of the container, holds the values of the new item
1545
+ // (e.g. add({ name: 'John', qty: 1 })). The 'name' prop is ambiguous:
1546
+ // it is treated as a value only when the container defines a nested
1547
+ // field called 'name'. (#657)
1548
+ const payloadKeys = lodash.isPlainObject(obj)
1549
+ ? Object.keys(obj).filter((k) => k !== FieldPropsEnum.key)
1550
+ : [];
1551
+ const isValuesPayload = !!nestedFields &&
1552
+ lodash.isPlainObject(obj) &&
1553
+ !lodash.has(obj, FieldPropsEnum.value) &&
1554
+ !lodash.has(obj, FieldPropsEnum.fields) &&
1555
+ payloadKeys.length > 0 &&
1556
+ (!lodash.has(obj, FieldPropsEnum.name) ||
1557
+ Object.prototype.hasOwnProperty.call(nestedFields, FieldPropsEnum.name));
1558
+ const $key = key ?? obj?.[FieldPropsEnum.key];
1559
+ const $fieldKey = $key !== void 0
1560
+ ? $key
1561
+ : !isValuesPayload && obj?.[FieldPropsEnum.name] !== void 0
1562
+ ? obj[FieldPropsEnum.name]
1563
+ : maxKey(this.fields);
1564
+ const $path = ($k) => [this.path, $k].join(".").replace(/^\.+/, "");
1565
+ const field = this.initField($fieldKey, $path($fieldKey), isValuesPayload ? tree[0] : lodash.merge(tree[0], obj), false, lodash.has(obj, FieldPropsEnum.value) ? obj[FieldPropsEnum.value] : void 0);
1486
1566
  const hasValues = lodash.has(obj, FieldPropsEnum.value) || lodash.has(obj, FieldPropsEnum.fields);
1487
- if (!hasValues) {
1567
+ if (isValuesPayload) {
1568
+ lodash.each(payloadKeys, (fieldName) => {
1569
+ const nestedField = field.select(fieldName);
1570
+ if (nestedField && !nestedField.hasNestedFields) {
1571
+ nestedField.set(FieldPropsEnum.value, obj[fieldName]);
1572
+ }
1573
+ });
1574
+ }
1575
+ else if (!hasValues) {
1488
1576
  // When the struct defines nested fields and obj is a plain object
1489
1577
  // without field-definition props (key, name), treat obj properties
1490
1578
  // directly as nested field values (e.g. add({ firstname: 'John' }))
@@ -1514,11 +1602,71 @@
1514
1602
  })));
1515
1603
  }
1516
1604
  }
1517
- this.$changed++;
1518
- this.state.form.$changed++;
1519
- execEvent && this.execHook(FieldPropsEnum.onAdd);
1520
1605
  return field;
1521
1606
  }
1607
+ /**
1608
+ Checks if the given path refers to an array container
1609
+ (a struct entry like `path[]` or `path[].subfield`)
1610
+ */
1611
+ isArrayContainer(path) {
1612
+ const struct = this.state.struct();
1613
+ if (!Array.isArray(struct))
1614
+ return false;
1615
+ const structPath = (path ?? "").replace(/\.\d+\./g, "[].");
1616
+ return struct.some((s) => typeof s === "string" && s.startsWith(`${structPath}[]`));
1617
+ }
1618
+ /**
1619
+ Checks if the field is a plain field holding an array value
1620
+ (not an array container with nested fields)
1621
+ */
1622
+ isPlainArrayField() {
1623
+ return (!this.hasNestedFields &&
1624
+ !this.isArrayContainer(this.path ?? "") &&
1625
+ Array.isArray(this.value));
1626
+ }
1627
+ /**
1628
+ Applies a value to a field: for fields with nested fields the value is
1629
+ applied to the nested fields, otherwise the field value is set
1630
+ */
1631
+ applyValue($field, value) {
1632
+ if (value === void 0)
1633
+ return;
1634
+ if (lodash.isPlainObject(value) && $field.fields.size) {
1635
+ lodash.each(value, (val, key) => {
1636
+ const $nested = $field.select(key);
1637
+ if ($nested) {
1638
+ $nested.fields.size
1639
+ ? this.applyValue($nested, val)
1640
+ : $nested.set(FieldPropsEnum.value, val);
1641
+ }
1642
+ });
1643
+ return;
1644
+ }
1645
+ $field.set(FieldPropsEnum.value, value);
1646
+ }
1647
+ /**
1648
+ Writes an array of values into an array container: the nested fields are
1649
+ created, updated and removed to match the given array
1650
+ */
1651
+ applyArrayValue($field, values) {
1652
+ // remove the nested fields which are not in the new values
1653
+ getObservableMapValues($field.fields).forEach(($f) => {
1654
+ if (Number($f.name) >= values.length) {
1655
+ $field.fields.delete($f.name);
1656
+ }
1657
+ });
1658
+ // create or update the nested fields
1659
+ values.forEach((value, index) => {
1660
+ const key = String(index);
1661
+ const $nested = $field.fields.get(key);
1662
+ if ($nested) {
1663
+ this.applyValue($nested, value);
1664
+ }
1665
+ else {
1666
+ $field.createField(lodash.isPlainObject(value) ? value : { [FieldPropsEnum.value]: value }, key);
1667
+ }
1668
+ });
1669
+ }
1522
1670
  /**
1523
1671
  Del Field
1524
1672
  */
@@ -2526,11 +2674,12 @@
2526
2674
  }
2527
2675
  get isDirty() {
2528
2676
  const value = this.changed ? this.value : this.initial;
2529
- return !lodash.isEqual(this.initial, value);
2677
+ // a soft deleted field differs from its initial state
2678
+ return !lodash.isEqual(this.initial, value) || this.deleted;
2530
2679
  }
2531
2680
  get isPristine() {
2532
2681
  const value = this.changed ? this.value : this.initial;
2533
- return lodash.isEqual(this.initial, value);
2682
+ return lodash.isEqual(this.initial, value) && !this.deleted;
2534
2683
  }
2535
2684
  get isEmpty() {
2536
2685
  if (this.hasNestedFields)
@@ -2561,27 +2710,33 @@
2561
2710
  : this.$touched;
2562
2711
  }
2563
2712
  get deleted() {
2564
- return this.hasNestedFields
2565
- ? this.check(FieldPropsEnum.deleted, true)
2566
- : this.$deleted;
2713
+ // an explicit flag on the field itself always wins
2714
+ if (this.$deleted)
2715
+ return true;
2716
+ return this.hasNestedFields ? this.check(FieldPropsEnum.deleted, true) : false;
2567
2717
  }
2568
2718
  /* ------------------------------------------------------------------ */
2569
2719
  /* EVENTS HANDLERS */
2570
2720
  sync = mobx.action((e, v = null) => {
2571
2721
  const $get = ($) => isBool($, this.value) ? $.target.checked : $.target.value;
2572
- // assume "v" or "e" are the values
2722
+ // resolve the value to be set
2723
+ let $value;
2573
2724
  if (lodash.isNil(e) || lodash.isNil(e.target)) {
2574
2725
  if (!lodash.isNil(v) && !lodash.isNil(v.target)) {
2575
2726
  v = $get(v); // eslint-disable-line
2576
2727
  }
2577
- this.value = $try(e, v);
2578
- return;
2728
+ $value = $try(e, v);
2579
2729
  }
2580
- if (!lodash.isNil(e.target)) {
2581
- this.value = $get(e);
2730
+ else {
2731
+ $value = $get(e);
2732
+ }
2733
+ // an array set on a field with nested fields is applied to the nested
2734
+ // fields, otherwise the value would be silently dropped
2735
+ if (this.hasNestedFields && Array.isArray($value)) {
2736
+ this.state.form.applyArrayValue(this, $value);
2582
2737
  return;
2583
2738
  }
2584
- this.value = e;
2739
+ this.value = $value;
2585
2740
  });
2586
2741
  onSync = (...args) => this.type === "file"
2587
2742
  ? this.onDrop(...args)
@@ -2785,7 +2940,13 @@
2785
2940
  nullable: this.$nullable,
2786
2941
  type: this.type,
2787
2942
  });
2788
- deep && this.each((field) => field.clear(deep));
2943
+ if (this.isArrayContainer(this.path ?? "") && Array.isArray(this.value)) {
2944
+ // an array container is emptied: the nested fields are removed
2945
+ getObservableMapValues(this.fields).forEach(($f) => this.fields.delete($f.name));
2946
+ }
2947
+ else {
2948
+ deep && this.each((field) => field.clear(deep));
2949
+ }
2789
2950
  this.state.options.get(OptionsEnum.validateOnClear, this)
2790
2951
  ? this.validate({
2791
2952
  showErrors: this.state.options.get(OptionsEnum.showErrorsOnClear, this),
@@ -2800,11 +2961,31 @@
2800
2961
  this.$changed = 0;
2801
2962
  this.files = undefined;
2802
2963
  const useDefaultValue = this.$default !== this.$initial;
2803
- if (useDefaultValue)
2804
- this.value = this.$default;
2805
- if (!useDefaultValue)
2806
- this.value = this.$initial;
2807
- deep && this.each((field) => field.reset(deep));
2964
+ const $target = useDefaultValue ? this.$default : this.$initial;
2965
+ if (this.isArrayContainer(this.path ?? "") &&
2966
+ !useDefaultValue &&
2967
+ Array.isArray($target)) {
2968
+ // an array container is rebuilt from its initial (or default) array:
2969
+ // the removed nested fields are restored and the added ones dropped
2970
+ const $values = $target;
2971
+ const $fields = getObservableMapValues(this.fields);
2972
+ const $match = $fields.length === $values.length &&
2973
+ $fields.every(($f, i) => lodash.isEqual($f.value, $values[i]));
2974
+ if (!$match) {
2975
+ $fields.forEach(($f) => this.fields.delete($f.name));
2976
+ $values.forEach((value, index) => this.createField(lodash.isPlainObject(value) ? value : { [FieldPropsEnum.value]: value }, String(index)));
2977
+ }
2978
+ else {
2979
+ deep && this.each((field) => field.reset(deep));
2980
+ }
2981
+ }
2982
+ else {
2983
+ if (useDefaultValue)
2984
+ this.value = this.$default;
2985
+ if (!useDefaultValue)
2986
+ this.value = this.$initial;
2987
+ deep && this.each((field) => field.reset(deep));
2988
+ }
2808
2989
  this.state.options.get(OptionsEnum.validateOnReset, this)
2809
2990
  ? this.validate({
2810
2991
  showErrors: this.state.options.get(OptionsEnum.showErrorsOnReset, this),