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.
package/src/Base.ts CHANGED
@@ -110,7 +110,15 @@ export default abstract class Base<
110
110
  set: action,
111
111
  add: action,
112
112
  del: action,
113
+ move: action,
113
114
  });
115
+
116
+ // Any reorder of the fields (also when `fields.move()` is called
117
+ // directly) flags the field and the form as changed. (#657)
118
+ this.fields.onMove = () => {
119
+ this.$changed++;
120
+ this.state.form.$changed++;
121
+ };
114
122
  }
115
123
 
116
124
  execHook = (name: string, fallback: any = {}, ...args: any[]): any =>
@@ -294,6 +302,7 @@ export default abstract class Base<
294
302
  path: string,
295
303
  data: any,
296
304
  update: boolean = false,
305
+ value?: any,
297
306
  ): any {
298
307
  const initial = this.state.get("current", "props");
299
308
  const struct = pathToStruct(path);
@@ -316,7 +325,10 @@ export default abstract class Base<
316
325
  };
317
326
 
318
327
  const props = {
319
- $value: get(initial[SeparatedPropsMode.values], path),
328
+ $value:
329
+ value !== void 0
330
+ ? value
331
+ : get(initial[SeparatedPropsMode.values], path),
320
332
  $computed: _try(SeparatedPropsMode.computed),
321
333
  $label: _try(SeparatedPropsMode.labels),
322
334
  $placeholder: _try(SeparatedPropsMode.placeholders),
@@ -455,11 +467,24 @@ export default abstract class Base<
455
467
  return transform(
456
468
  $fields,
457
469
  (check: any, field: any) => {
470
+ // Array containers are skipped because most of their props are
471
+ // computed recursively on their nested fields. Some props are not
472
+ // recursive (isDirty/isPristine compare the container value with
473
+ // its initial value) or must aggregate the container itself
474
+ // (isValid/hasError), so they are taken into account for array
475
+ // containers too. (#656, #657)
476
+ const isContainerProp = (
477
+ [
478
+ FieldPropsEnum.isValid,
479
+ FieldPropsEnum.hasError,
480
+ FieldPropsEnum.isDirty,
481
+ FieldPropsEnum.isPristine,
482
+ ] as string[]
483
+ ).includes(prop);
458
484
  if (
485
+ isContainerProp ||
459
486
  !field.fields.size ||
460
- !Array.isArray(field.initial) ||
461
- prop === FieldPropsEnum.isValid ||
462
- prop === FieldPropsEnum.hasError
487
+ !Array.isArray(field.initial)
463
488
  ) {
464
489
  check.push(field[prop]);
465
490
  }
@@ -531,6 +556,24 @@ export default abstract class Base<
531
556
  );
532
557
 
533
558
  if ($field != null && field !== void 0) {
559
+ // an array provided for a field which is not an array container
560
+ // is the field value itself (not a set of nested fields)
561
+ const $rawValue = get(raw, $path);
562
+ const $absPath = [this.path ?? "", $path].join(".").replace(/^\.+/, "");
563
+ if (Array.isArray($rawValue) && !this.isArrayContainer($absPath)) {
564
+ $field.value = parseInput(
565
+ applyInputConverterOnUpdate ? $field.$input : (val) => val,
566
+ {
567
+ fallbackValueOption: this.state.options.get(
568
+ OptionsEnum.fallbackValue,
569
+ this,
570
+ ),
571
+ separated: $rawValue,
572
+ },
573
+ );
574
+ return;
575
+ }
576
+
534
577
  if (Array.isArray($field.values())) {
535
578
  const n: number = Math.max(
536
579
  -1,
@@ -586,15 +629,22 @@ export default abstract class Base<
586
629
  // init field into the container field
587
630
  $container.$changed++;
588
631
  $container.state.form.$changed++;
589
- $container.initField($key, $newFieldPath, field, true);
632
+ // the value provided by the update wins over the declared values
633
+ $container.initField(
634
+ $key,
635
+ $newFieldPath,
636
+ field,
637
+ true,
638
+ get(raw, $path),
639
+ );
590
640
  } else if (recursion) {
591
641
  if (has(field, FieldPropsEnum.fields) && field.fields != null) {
592
642
  // handle nested fields if defined
593
- this.deepUpdate(field.fields, $path);
643
+ this.deepUpdate(field.fields, $path, true, raw);
594
644
  } else {
595
645
  // handle nested fields if undefined or null
596
646
  const $fields = pathToFieldsTree(this.state.struct(), $path);
597
- this.deepUpdate($fields, $path, false);
647
+ this.deepUpdate($fields, $path, false, raw);
598
648
  }
599
649
  }
600
650
  });
@@ -868,24 +918,99 @@ export default abstract class Base<
868
918
  return this;
869
919
  }
870
920
 
871
- let key;
872
- if (has(obj, FieldPropsEnum.key)) key = obj.key;
873
- if (has(obj, FieldPropsEnum.name)) key = obj.name;
874
- if (!key) key = maxKey(this.fields);
921
+ // a plain field holding an array value gets the new item appended
922
+ if (
923
+ this.isPlainArrayField() &&
924
+ !has(obj, FieldPropsEnum.key) &&
925
+ !has(obj, FieldPropsEnum.name)
926
+ ) {
927
+ const $values = Array.isArray((this as any).value)
928
+ ? [...((this as any).value as any[])]
929
+ : [];
930
+ $values.push(
931
+ has(obj, FieldPropsEnum.value)
932
+ ? obj[FieldPropsEnum.value]
933
+ : defaultValue({
934
+ fallbackValueOption: this.state.options.get(
935
+ OptionsEnum.fallbackValue,
936
+ this,
937
+ ),
938
+ nullable: (this as any).$nullable,
939
+ type: (this as any).type,
940
+ }),
941
+ );
942
+ this.set(FieldPropsEnum.value, $values);
943
+ this.$changed++;
944
+ this.state.form.$changed++;
945
+ execEvent && this.execHook(FieldPropsEnum.onAdd);
946
+ return this;
947
+ }
948
+
949
+ const field = this.createField(obj);
950
+
951
+ this.$changed++;
952
+ this.state.form.$changed++;
953
+ execEvent && this.execHook(FieldPropsEnum.onAdd);
954
+ return field;
955
+ }
875
956
 
876
- const $path = ($key: string) =>
877
- [this.path, $key].join(".").replace(/^\.+/, "");
957
+ /**
958
+ Create a new field into this container (no change counters, no hooks)
959
+ */
960
+ createField(obj?: any, key?: string): any {
878
961
  const tree = pathToFieldsTree(
879
962
  this.state.struct(),
880
963
  this.path ?? "",
881
964
  0,
882
965
  true,
883
966
  );
884
- const field = this.initField(key, $path(key), merge(tree[0], obj));
967
+ const nestedFields = tree[0]?.fields ?? null;
968
+
969
+ // An object without field-definition props, whose keys match the
970
+ // nested fields of the container, holds the values of the new item
971
+ // (e.g. add({ name: 'John', qty: 1 })). The 'name' prop is ambiguous:
972
+ // it is treated as a value only when the container defines a nested
973
+ // field called 'name'. (#657)
974
+ const payloadKeys = isPlainObject(obj)
975
+ ? Object.keys(obj).filter((k) => k !== FieldPropsEnum.key)
976
+ : [];
977
+ const isValuesPayload =
978
+ !!nestedFields &&
979
+ isPlainObject(obj) &&
980
+ !has(obj, FieldPropsEnum.value) &&
981
+ !has(obj, FieldPropsEnum.fields) &&
982
+ payloadKeys.length > 0 &&
983
+ (!has(obj, FieldPropsEnum.name) ||
984
+ Object.prototype.hasOwnProperty.call(nestedFields, FieldPropsEnum.name));
985
+
986
+ const $key = key ?? obj?.[FieldPropsEnum.key];
987
+ const $fieldKey: any =
988
+ $key !== void 0
989
+ ? $key
990
+ : !isValuesPayload && obj?.[FieldPropsEnum.name] !== void 0
991
+ ? obj[FieldPropsEnum.name]
992
+ : maxKey(this.fields);
993
+
994
+ const $path = ($k: any) =>
995
+ [this.path, $k].join(".").replace(/^\.+/, "");
996
+ const field = this.initField(
997
+ $fieldKey,
998
+ $path($fieldKey),
999
+ isValuesPayload ? tree[0] : merge(tree[0], obj),
1000
+ false,
1001
+ has(obj, FieldPropsEnum.value) ? obj[FieldPropsEnum.value] : void 0,
1002
+ );
885
1003
  const hasValues =
886
1004
  has(obj, FieldPropsEnum.value) || has(obj, FieldPropsEnum.fields);
887
1005
 
888
- if (!hasValues) {
1006
+ if (isValuesPayload) {
1007
+ each(payloadKeys, (fieldName) => {
1008
+ const nestedField = field.select(fieldName);
1009
+ if (nestedField && !nestedField.hasNestedFields) {
1010
+ nestedField.set(FieldPropsEnum.value, (obj as any)[fieldName]);
1011
+ }
1012
+ });
1013
+ } else if (!hasValues) {
889
1014
  // When the struct defines nested fields and obj is a plain object
890
1015
  // without field-definition props (key, name), treat obj properties
891
1016
  // directly as nested field values (e.g. add({ firstname: 'John' }))
@@ -925,12 +1050,81 @@ export default abstract class Base<
925
1050
  }
926
1051
  }
927
1052
 
928
- this.$changed++;
929
- this.state.form.$changed++;
930
- execEvent && this.execHook(FieldPropsEnum.onAdd);
931
1053
  return field;
932
1054
  }
933
1055
 
1056
+ /**
1057
+ Checks if the given path refers to an array container
1058
+ (a struct entry like `path[]` or `path[].subfield`)
1059
+ */
1060
+ isArrayContainer(path: string): boolean {
1061
+ const struct = this.state.struct();
1062
+ if (!Array.isArray(struct)) return false;
1063
+ const structPath = (path ?? "").replace(/\.\d+\./g, "[].");
1064
+ return struct.some(
1065
+ (s: any) => typeof s === "string" && s.startsWith(`${structPath}[]`),
1066
+ );
1067
+ }
1068
+
1069
+ /**
1070
+ Checks if the field is a plain field holding an array value
1071
+ (not an array container with nested fields)
1072
+ */
1073
+ isPlainArrayField(): boolean {
1074
+ return (
1075
+ !this.hasNestedFields &&
1076
+ !this.isArrayContainer(this.path ?? "") &&
1077
+ Array.isArray((this as any).value)
1078
+ );
1079
+ }
1080
+
1081
+ /**
1082
+ Applies a value to a field: for fields with nested fields the value is
1083
+ applied to the nested fields, otherwise the field value is set
1084
+ */
1085
+ applyValue($field: any, value: any): void {
1086
+ if (value === void 0) return;
1087
+ if (isPlainObject(value) && $field.fields.size) {
1088
+ each(value, (val, key) => {
1089
+ const $nested = $field.select(key);
1090
+ if ($nested) {
1091
+ $nested.fields.size
1092
+ ? this.applyValue($nested, val)
1093
+ : $nested.set(FieldPropsEnum.value, val);
1094
+ }
1095
+ });
1096
+ return;
1097
+ }
1098
+ $field.set(FieldPropsEnum.value, value);
1099
+ }
1100
+
1101
+ /**
1102
+ Writes an array of values into an array container: the nested fields are
1103
+ created, updated and removed to match the given array
1104
+ */
1105
+ applyArrayValue($field: any, values: any[]): void {
1106
+ // remove the nested fields which are not in the new values
1107
+ getObservableMapValues($field.fields).forEach(($f: any) => {
1108
+ if (Number($f.name) >= values.length) {
1109
+ $field.fields.delete($f.name);
1110
+ }
1111
+ });
1112
+
1113
+ // create or update the nested fields
1114
+ values.forEach((value, index) => {
1115
+ const key = String(index);
1116
+ const $nested = $field.fields.get(key);
1117
+ if ($nested) {
1118
+ this.applyValue($nested, value);
1119
+ } else {
1120
+ $field.createField(
1121
+ isPlainObject(value) ? value : { [FieldPropsEnum.value]: value },
1122
+ key,
1123
+ );
1124
+ }
1125
+ });
1126
+ }
1127
+
934
1128
  /**
935
1129
  Del Field
936
1130
  */
package/src/Field.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  isEvent,
21
21
  pathToStruct,
22
22
  isArrayFromStruct,
23
+ getObservableMapValues,
23
24
  } from "./utils";
24
25
 
25
26
  import { parseInput, parseCheckOutput, defaultValue } from "./parser";
@@ -503,12 +504,13 @@ export default class Field<T = any> extends Base<Record<string, any>> implements
503
504
 
504
505
  get isDirty(): boolean {
505
506
  const value = this.changed ? this.value : this.initial;
506
- return !isEqual(this.initial, value);
507
+ // a soft deleted field differs from its initial state
508
+ return !isEqual(this.initial, value) || this.deleted;
507
509
  }
508
510
 
509
511
  get isPristine(): boolean {
510
512
  const value = this.changed ? this.value : this.initial;
511
- return isEqual(this.initial, value);
513
+ return isEqual(this.initial, value) && !this.deleted;
512
514
  }
513
515
 
514
516
  get isEmpty(): boolean {
@@ -539,9 +541,9 @@ export default class Field<T = any> extends Base<Record<string, any>> implements
539
541
  }
540
542
 
541
543
  get deleted(): boolean {
542
- return this.hasNestedFields
543
- ? this.check(FieldPropsEnum.deleted, true)
544
- : this.$deleted;
544
+ // an explicit flag on the field itself always wins
545
+ if (this.$deleted) return true;
546
+ return this.hasNestedFields ? this.check(FieldPropsEnum.deleted, true) : false;
545
547
  }
546
548
 
547
549
  /* ------------------------------------------------------------------ */
@@ -551,22 +553,25 @@ export default class Field<T = any> extends Base<Record<string, any>> implements
551
553
  const $get = ($: any) =>
552
554
  isBool($, this.value) ? $.target.checked : $.target.value;
553
555
 
554
- // assume "v" or "e" are the values
556
+ // resolve the value to be set
557
+ let $value: any;
555
558
  if (isNil(e) || isNil(e.target)) {
556
559
  if (!isNil(v) && !isNil(v.target)) {
557
560
  v = $get(v); // eslint-disable-line
558
561
  }
559
-
560
- this.value = $try(e, v);
561
- return;
562
+ $value = $try(e, v);
563
+ } else {
564
+ $value = $get(e);
562
565
  }
563
566
 
564
- if (!isNil(e.target)) {
565
- this.value = $get(e);
567
+ // an array set on a field with nested fields is applied to the nested
568
+ // fields, otherwise the value would be silently dropped
569
+ if (this.hasNestedFields && Array.isArray($value)) {
570
+ (this.state.form as any).applyArrayValue(this, $value);
566
571
  return;
567
572
  }
568
573
 
569
- this.value = e;
574
+ this.value = $value;
570
575
  });
571
576
 
572
577
  onSync = (...args: any) =>
@@ -876,7 +881,14 @@ export default class Field<T = any> extends Base<Record<string, any>> implements
876
881
  type: this.type,
877
882
  });
878
883
 
879
- deep && this.each((field: FieldInterface) => field.clear(deep));
884
+ if (this.isArrayContainer(this.path ?? "") && Array.isArray(this.value)) {
885
+ // an array container is emptied: the nested fields are removed
886
+ getObservableMapValues(this.fields).forEach(($f: any) =>
887
+ this.fields.delete($f.name),
888
+ );
889
+ } else {
890
+ deep && this.each((field: FieldInterface) => field.clear(deep));
891
+ }
880
892
 
881
893
  this.state.options.get(OptionsEnum.validateOnClear, this)
882
894
  ? this.validate({
@@ -897,10 +909,37 @@ export default class Field<T = any> extends Base<Record<string, any>> implements
897
909
  this.files = undefined;
898
910
 
899
911
  const useDefaultValue = this.$default !== this.$initial;
900
- if (useDefaultValue) this.value = this.$default;
901
- if (!useDefaultValue) this.value = this.$initial;
912
+ const $target = useDefaultValue ? this.$default : this.$initial;
913
+
914
+ if (
915
+ this.isArrayContainer(this.path ?? "") &&
916
+ !useDefaultValue &&
917
+ Array.isArray($target)
918
+ ) {
919
+ // an array container is rebuilt from its initial (or default) array:
920
+ // the removed nested fields are restored and the added ones dropped
921
+ const $values = $target as any[];
922
+ const $fields = getObservableMapValues(this.fields);
923
+ const $match =
924
+ $fields.length === $values.length &&
925
+ $fields.every(($f: any, i: number) => isEqual($f.value, $values[i]));
926
+ if (!$match) {
927
+ $fields.forEach(($f: any) => this.fields.delete($f.name));
928
+ $values.forEach((value, index) =>
929
+ this.createField(
930
+ isPlainObject(value) ? value : { [FieldPropsEnum.value]: value },
931
+ String(index),
932
+ ),
933
+ );
934
+ } else {
935
+ deep && this.each((field: FieldInterface) => field.reset(deep));
936
+ }
937
+ } else {
938
+ if (useDefaultValue) this.value = this.$default;
939
+ if (!useDefaultValue) this.value = this.$initial;
902
940
 
903
- deep && this.each((field: FieldInterface) => field.reset(deep));
941
+ deep && this.each((field: FieldInterface) => field.reset(deep));
942
+ }
904
943
 
905
944
  this.state.options.get(OptionsEnum.validateOnReset, this)
906
945
  ? this.validate({