@strictly/react-form 0.0.10 → 0.0.12

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.
Files changed (54) hide show
  1. package/.out/core/mobx/field_adapter_builder.js +18 -6
  2. package/.out/core/mobx/{form_presenter.d.ts → form_model.d.ts} +15 -21
  3. package/.out/core/mobx/form_model.js +513 -0
  4. package/.out/core/mobx/hooks.d.ts +6 -25
  5. package/.out/core/mobx/hooks.js +14 -50
  6. package/.out/core/mobx/merge_field_adapters_with_validators.js +1 -5
  7. package/.out/core/mobx/specs/fixtures.js +2 -1
  8. package/.out/core/mobx/specs/{form_presenter.tests.js → form_model.tests.js} +52 -43
  9. package/.out/core/mobx/specs/sub_form_field_adapters.tests.js +2 -1
  10. package/.out/core/mobx/types.d.ts +4 -4
  11. package/.out/field_converters/integer_to_string_converter.js +12 -4
  12. package/.out/field_converters/maybe_identity_converter.js +12 -4
  13. package/.out/field_converters/nullable_to_boolean_converter.js +24 -7
  14. package/.out/field_converters/select_value_type_converter.js +36 -12
  15. package/.out/index.d.ts +1 -1
  16. package/.out/index.js +1 -1
  17. package/.out/mantine/create_checkbox.js +8 -4
  18. package/.out/mantine/create_fields_view.js +7 -4
  19. package/.out/mantine/create_form.js +1 -1
  20. package/.out/mantine/create_radio_group.js +8 -4
  21. package/.out/mantine/create_text_input.js +8 -4
  22. package/.out/mantine/create_value_input.js +8 -4
  23. package/.out/mantine/hooks.js +218 -92
  24. package/.out/mantine/specs/checkbox_hooks.stories.js +13 -1
  25. package/.out/mantine/specs/checkbox_hooks.tests.js +22 -9
  26. package/.out/mantine/specs/fields_view_hooks.stories.js +15 -2
  27. package/.out/mantine/specs/fields_view_hooks.tests.js +12 -3
  28. package/.out/mantine/specs/radio_group_hooks.stories.js +13 -1
  29. package/.out/mantine/specs/radio_group_hooks.tests.js +23 -10
  30. package/.out/mantine/specs/select_hooks.stories.js +13 -1
  31. package/.out/mantine/specs/text_input_hooks.stories.js +13 -1
  32. package/.out/mantine/specs/text_input_hooks.tests.js +18 -7
  33. package/.out/mantine/specs/value_input_hooks.stories.js +14 -2
  34. package/.out/tsconfig.tsbuildinfo +1 -1
  35. package/.out/tsup.config.js +2 -9
  36. package/.out/types/merge_validators.js +1 -4
  37. package/.out/util/partial.js +5 -5
  38. package/.out/vitest.workspace.js +2 -10
  39. package/.turbo/turbo-build.log +9 -9
  40. package/.turbo/turbo-check-types.log +1 -1
  41. package/.turbo/turbo-release$colon$exports.log +1 -1
  42. package/core/mobx/{form_presenter.ts → form_model.ts} +287 -329
  43. package/core/mobx/hooks.tsx +26 -123
  44. package/core/mobx/specs/{form_presenter.tests.ts → form_model.tests.ts} +101 -94
  45. package/core/mobx/types.ts +12 -12
  46. package/dist/index.cjs +639 -600
  47. package/dist/index.d.cts +51 -73
  48. package/dist/index.d.ts +51 -73
  49. package/dist/index.js +644 -601
  50. package/index.ts +1 -1
  51. package/mantine/hooks.tsx +2 -0
  52. package/package.json +1 -1
  53. package/.out/core/mobx/form_presenter.js +0 -422
  54. /package/.out/core/mobx/specs/{form_presenter.tests.d.ts → form_model.tests.d.ts} +0 -0
package/index.ts CHANGED
@@ -3,7 +3,7 @@ export * from './core/mobx/field_adapter_builder'
3
3
  export * from './core/mobx/field_adapters_of_values'
4
4
  export * from './core/mobx/flattened_adapters_of_fields'
5
5
  export * from './core/mobx/form_fields_of_field_adapters'
6
- export * from './core/mobx/form_presenter'
6
+ export * from './core/mobx/form_model'
7
7
  export * from './core/mobx/hooks'
8
8
  export * from './core/mobx/merge_field_adapters_with_two_way_converter'
9
9
  export * from './core/mobx/merge_field_adapters_with_validators'
package/mantine/hooks.tsx CHANGED
@@ -431,4 +431,6 @@ class MantineFormImpl<
431
431
  this,
432
432
  ) as unknown as MantineFieldComponent<FormProps<ValueTypeOfField<F[K]>>, P, never>
433
433
  }
434
+
435
+ // TODO have an option to bind to a Text/(value: T) => JSX.Element for viewing form fields
434
436
  }
package/package.json CHANGED
@@ -70,7 +70,7 @@
70
70
  "test:watch": "vitest"
71
71
  },
72
72
  "type": "module",
73
- "version": "0.0.10",
73
+ "version": "0.0.12",
74
74
  "exports": {
75
75
  ".": {
76
76
  "import": {
@@ -1,422 +0,0 @@
1
- import { assertExists, assertExistsAndReturn, assertState, checkValidNumber, map, toArray, UnreachableError, } from '@strictly/base';
2
- import { flattenAccessorsOfType, flattenTypesOfType, flattenValuesOfType, flattenValueTo, jsonPathPop, mobxCopy, valuePathToTypePath, } from '@strictly/define';
3
- import { computed, observable, runInAction, } from 'mobx';
4
- import { UnreliableFieldConversionType, } from 'types/field_converters';
5
- export class FormPresenter {
6
- type;
7
- adapters;
8
- constructor(type, adapters) {
9
- this.type = type;
10
- this.adapters = adapters;
11
- }
12
- maybeGetAdapterForValuePath(valuePath) {
13
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
14
- const typePath = valuePathToTypePath(this.type, valuePath, true);
15
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
16
- return this.adapters[typePath];
17
- }
18
- getAdapterForValuePath(valuePath) {
19
- return assertExistsAndReturn(this.maybeGetAdapterForValuePath(valuePath), 'expected adapter to be defined {}', valuePath);
20
- }
21
- typePath(valuePath) {
22
- return valuePathToTypePath(this.type, valuePath, true);
23
- }
24
- setFieldValueAndValidate(model, valuePath, value) {
25
- return this.internalSetFieldValue(model, valuePath, value, true);
26
- }
27
- setFieldValue(model, valuePath, value) {
28
- return this.internalSetFieldValue(model, valuePath, value, false);
29
- }
30
- addListItem(model, valuePath,
31
- // TODO can this type be simplified?
32
- elementValue = null, index) {
33
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
34
- const listValuePath = valuePath;
35
- const accessor = model.accessors[valuePath];
36
- const listTypePath = this.typePath(valuePath);
37
- const definedIndex = index ?? accessor.value.length;
38
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
39
- const elementTypePath = `${listTypePath}.*`;
40
- const elementAdapter = assertExistsAndReturn(this.adapters[elementTypePath], 'no adapter specified for list {} ({})', elementTypePath, valuePath);
41
- // TODO validation on new elements
42
- const element = elementValue != null
43
- ? elementValue[0]
44
- : elementAdapter.create(
45
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
46
- elementTypePath, model.value);
47
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
- const originalList = accessor.value;
49
- const newList = [
50
- ...originalList.slice(0, definedIndex),
51
- element,
52
- ...originalList.slice(definedIndex),
53
- ];
54
- // shuffle the overrides around to account for new indices
55
- // to so this we need to sort the array indices in descending order
56
- const targetPaths = Object.keys(model.fieldOverrides).filter(function (v) {
57
- return v.startsWith(`${listValuePath}.`);
58
- }).map(function (v) {
59
- const parts = v.substring(listValuePath.length + 1).split('.');
60
- const index = parseInt(parts[0]);
61
- return [
62
- index,
63
- parts.slice(1),
64
- ];
65
- }).filter(function ([index]) {
66
- return index >= definedIndex;
67
- }).sort(function ([a], [b]) {
68
- // descending
69
- return b - a;
70
- });
71
- runInAction(function () {
72
- targetPaths.forEach(function ([index, postfix,]) {
73
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
74
- const fromJsonPath = [
75
- listValuePath,
76
- `${index}`,
77
- ...postfix,
78
- ].join('.');
79
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
80
- const toJsonPath = [
81
- listValuePath,
82
- `${index + 1}`,
83
- ...postfix,
84
- ].join('.');
85
- const fieldOverride = model.fieldOverrides[fromJsonPath];
86
- delete model.fieldOverrides[fromJsonPath];
87
- model.fieldOverrides[toJsonPath] = fieldOverride;
88
- const error = model.errors[fromJsonPath];
89
- delete model.errors[fromJsonPath];
90
- model.errors[toJsonPath] = error;
91
- });
92
- accessor.set(newList);
93
- // delete any value overrides so the new list isn't shadowed
94
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
95
- delete model.fieldOverrides[listValuePath];
96
- });
97
- }
98
- removeListItem(model, elementValuePath) {
99
- const [listValuePath, elementIndexString,] = assertExistsAndReturn(jsonPathPop(elementValuePath), 'expected a path with two or more segments {}', elementValuePath);
100
- const accessor = model.accessors[listValuePath];
101
- const elementIndex = checkValidNumber(parseInt(elementIndexString), 'unexpected index {} ({})', elementIndexString, elementValuePath);
102
- const newList = [...accessor.value];
103
- assertState(elementIndex >= 0 && elementIndex < newList.length, 'invalid index from path {} ({})', elementIndex, elementValuePath);
104
- newList.splice(elementIndex, 1);
105
- // shuffle the overrides around to account for new indices
106
- // to so this we need to sort the array indices in descending order
107
- const targetPaths = Object.keys(model.fieldOverrides).filter(function (v) {
108
- return v.startsWith(`${listValuePath}.`);
109
- }).map(function (v) {
110
- const parts = v.substring(listValuePath.length + 1).split('.');
111
- const index = parseInt(parts[0]);
112
- return [
113
- index,
114
- parts.slice(1),
115
- ];
116
- }).filter(function ([index]) {
117
- return index > elementIndex;
118
- }).sort(function ([a], [b]) {
119
- // ascending
120
- return a - b;
121
- });
122
- runInAction(function () {
123
- targetPaths.forEach(function ([index, postfix,]) {
124
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
125
- const fromJsonPath = [
126
- listValuePath,
127
- `${index}`,
128
- ...postfix,
129
- ].join('.');
130
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
131
- const toJsonPath = [
132
- listValuePath,
133
- `${index - 1}`,
134
- ...postfix,
135
- ].join('.');
136
- const fieldOverride = model.fieldOverrides[fromJsonPath];
137
- delete model.fieldOverrides[fromJsonPath];
138
- model.fieldOverrides[toJsonPath] = fieldOverride;
139
- const error = model.errors[fromJsonPath];
140
- delete model.errors[fromJsonPath];
141
- model.errors[toJsonPath] = error;
142
- });
143
- accessor.set(newList);
144
- // delete any value overrides so the new list isn't shadowed
145
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
146
- delete model.fieldOverrides[listValuePath];
147
- });
148
- }
149
- internalSetFieldValue(model, valuePath, value, displayValidation) {
150
- const { revert } = this.getAdapterForValuePath(valuePath);
151
- assertExists(revert, 'setting value not supported {}', valuePath);
152
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
153
- const conversion = revert(value, valuePath, model.value);
154
- const accessor = model.getAccessorForValuePath(valuePath);
155
- return runInAction(() => {
156
- model.fieldOverrides[valuePath] = [value];
157
- switch (conversion.type) {
158
- case UnreliableFieldConversionType.Failure:
159
- if (displayValidation) {
160
- model.errors[valuePath] = conversion.error;
161
- }
162
- if (conversion.value != null && accessor != null) {
163
- accessor.set(conversion.value[0]);
164
- }
165
- return false;
166
- case UnreliableFieldConversionType.Success:
167
- delete model.errors[valuePath];
168
- accessor?.set(conversion.value);
169
- return true;
170
- default:
171
- throw new UnreachableError(conversion);
172
- }
173
- });
174
- }
175
- clearFieldError(model, valuePath) {
176
- const fieldOverride = model.fieldOverrides[valuePath];
177
- if (fieldOverride != null) {
178
- runInAction(function () {
179
- delete model.errors[valuePath];
180
- });
181
- }
182
- }
183
- clearFieldValue(model, valuePath) {
184
- const typePath = this.typePath(valuePath);
185
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
186
- const adapter = this.adapters[typePath];
187
- if (adapter == null) {
188
- return;
189
- }
190
- const { convert, create, } = adapter;
191
- const value = create(valuePath, model.value);
192
- const { value: displayValue, } = convert(value, valuePath, model.value);
193
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
194
- const key = valuePath;
195
- runInAction(function () {
196
- model.fieldOverrides[key] = [displayValue];
197
- });
198
- }
199
- clearAll(model, value) {
200
- runInAction(() => {
201
- model.errors = {};
202
- // TODO this isn't correct, should reload from value
203
- model.fieldOverrides = {};
204
- model.value = mobxCopy(this.type, value);
205
- });
206
- }
207
- isValuePathActive(model, valuePath) {
208
- const values = flattenValuesOfType(this.type, model.value);
209
- const keys = new Set(Object.keys(values));
210
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
211
- return keys.has(valuePath);
212
- }
213
- validateField(model, valuePath, ignoreDefaultValue = false) {
214
- const { convert, revert, create, } = this.getAdapterForValuePath(valuePath);
215
- const fieldOverride = model.fieldOverrides[valuePath];
216
- const accessor = model.getAccessorForValuePath(valuePath);
217
- const { value: storedValue, } = convert(accessor != null
218
- ? accessor.value
219
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
220
- : create(valuePath, model.value),
221
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
222
- valuePath, model.value);
223
- const value = fieldOverride != null
224
- ? fieldOverride[0]
225
- : storedValue;
226
- const dirty = storedValue !== value;
227
- assertExists(revert, 'changing field directly not supported {}', valuePath);
228
- if (ignoreDefaultValue) {
229
- const { value: defaultDisplayValue, } = convert(create(valuePath, model.value), valuePath, model.value);
230
- if (defaultDisplayValue === value) {
231
- return true;
232
- }
233
- }
234
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
235
- const conversion = revert(value, valuePath, model.value);
236
- return runInAction(function () {
237
- switch (conversion.type) {
238
- case UnreliableFieldConversionType.Failure:
239
- model.errors[valuePath] = conversion.error;
240
- if (conversion.value != null && accessor != null && dirty) {
241
- accessor.set(conversion.value[0]);
242
- }
243
- return false;
244
- case UnreliableFieldConversionType.Success:
245
- delete model.errors[valuePath];
246
- if (accessor != null && dirty) {
247
- accessor.set(conversion.value);
248
- }
249
- return true;
250
- default:
251
- throw new UnreachableError(conversion);
252
- }
253
- });
254
- }
255
- validateAll(model) {
256
- // sort keys shortest to longest so parent changes don't overwrite child changes
257
- const accessors = toArray(model.accessors).toSorted(function ([a], [b]) {
258
- return a.length - b.length;
259
- });
260
- return runInAction(() => {
261
- return accessors.reduce((success, [valuePath, accessor,]) => {
262
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
263
- const adapterPath = valuePath;
264
- const adapter = this.maybeGetAdapterForValuePath(adapterPath);
265
- if (adapter == null) {
266
- // no adapter == there should be nothing specified for this field
267
- return success;
268
- }
269
- const { convert, revert, } = adapter;
270
- if (revert == null) {
271
- // no convert method means this field is immutable
272
- return success;
273
- }
274
- const fieldOverride = model.fieldOverrides[adapterPath];
275
- const { value: storedValue, } = convert(accessor.value, valuePath, model.value);
276
- const value = fieldOverride != null
277
- ? fieldOverride[0]
278
- : storedValue;
279
- // TODO more nuanced comparison
280
- const dirty = fieldOverride != null && fieldOverride[0] !== storedValue;
281
- const conversion = revert(value, valuePath, model.value);
282
- switch (conversion.type) {
283
- case UnreliableFieldConversionType.Failure:
284
- model.errors[adapterPath] = conversion.error;
285
- if (conversion.value != null && dirty) {
286
- accessor.set(conversion.value[0]);
287
- }
288
- return false;
289
- case UnreliableFieldConversionType.Success:
290
- if (dirty) {
291
- accessor.set(conversion.value);
292
- }
293
- delete model.errors[adapterPath];
294
- return success;
295
- default:
296
- throw new UnreachableError(conversion);
297
- }
298
- }, true);
299
- });
300
- }
301
- }
302
- export class FormModel {
303
- type;
304
- adapters;
305
- @observable.ref
306
- accessor value;
307
- @observable.shallow
308
- accessor fieldOverrides;
309
- @observable.shallow
310
- accessor errors = {};
311
- flattenedTypeDefs;
312
- constructor(type, value, adapters) {
313
- this.type = type;
314
- this.adapters = adapters;
315
- this.value = mobxCopy(type, value);
316
- this.flattenedTypeDefs = flattenTypesOfType(type);
317
- // pre-populate field overrides for consistent behavior when default information is overwritten
318
- // then returned to
319
- const conversions = flattenValueTo(type, this.value, () => { }, (_t, value, _setter, typePath, valuePath) => {
320
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
321
- const adapter = this.adapters[typePath];
322
- if (adapter == null) {
323
- return;
324
- }
325
- const { convert, revert, } = adapter;
326
- if (revert == null) {
327
- // no need to store a temporary value if the value cannot be written back
328
- return;
329
- }
330
- return convert(value, valuePath, this.value);
331
- });
332
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
333
- this.fieldOverrides = map(conversions, function (_k, v) {
334
- return v && [v.value];
335
- });
336
- }
337
- @computed
338
- get fields() {
339
- return new Proxy(this.knownFields, {
340
- get: (target, prop) => {
341
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
342
- const field = target[prop];
343
- if (field != null) {
344
- return field;
345
- }
346
- if (typeof prop === 'string') {
347
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
348
- return this.maybeSynthesizeFieldByValuePath(prop);
349
- }
350
- },
351
- });
352
- }
353
- @computed
354
- get knownFields() {
355
- return flattenValueTo(this.type, this.value, () => { },
356
- // TODO swap these to valuePath, typePath in flatten
357
- (_t, _v, _setter, typePath, valuePath) => {
358
- return this.synthesizeFieldByPaths(
359
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
360
- valuePath,
361
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
362
- typePath);
363
- });
364
- }
365
- maybeSynthesizeFieldByValuePath(valuePath) {
366
- let typePath;
367
- try {
368
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
369
- typePath = valuePathToTypePath(this.type,
370
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
371
- valuePath, true);
372
- }
373
- catch (e) {
374
- // TODO make jsonValuePathToTypePath return null in the event of an invalid
375
- // value path instead of throwing an exception
376
- // assume that the path was invalid
377
- return;
378
- }
379
- return this.synthesizeFieldByPaths(valuePath, typePath);
380
- }
381
- synthesizeFieldByPaths(valuePath, typePath) {
382
- const adapter = this.adapters[typePath];
383
- if (adapter == null) {
384
- // invalid path, which can happen
385
- return;
386
- }
387
- const { convert, create, } = adapter;
388
- const fieldOverride = this.fieldOverrides[valuePath];
389
- const accessor = this.getAccessorForValuePath(valuePath);
390
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
391
- const fieldTypeDef = this.flattenedTypeDefs[typePath];
392
- const { value, required, readonly, } = convert(accessor != null
393
- ? accessor.value
394
- : fieldTypeDef != null
395
- ? mobxCopy(fieldTypeDef,
396
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
397
- create(valuePath, this.value))
398
- // fake values can't be copied
399
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
400
- : create(valuePath, this.value),
401
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
402
- valuePath, this.value);
403
- const error = this.errors[valuePath];
404
- return {
405
- value: fieldOverride != null ? fieldOverride[0] : value,
406
- error,
407
- readonly,
408
- required,
409
- };
410
- }
411
- getAccessorForValuePath(valuePath) {
412
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
413
- return this.accessors[valuePath];
414
- }
415
- @computed
416
- // should only be referenced internally, so loosely typed
417
- get accessors() {
418
- return flattenAccessorsOfType(this.type, this.value, (value) => {
419
- this.value = mobxCopy(this.type, value);
420
- });
421
- }
422
- }