@strictly/react-form 0.0.8 → 0.0.10
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/.out/core/mobx/field_adapter_builder.d.ts +4 -0
- package/.out/core/mobx/field_adapter_builder.js +31 -0
- package/.out/core/mobx/form_presenter.d.ts +5 -5
- package/.out/core/mobx/form_presenter.js +8 -6
- package/.out/core/mobx/hooks.d.ts +24 -4
- package/.out/core/mobx/hooks.js +24 -3
- package/.out/core/mobx/specs/form_presenter.tests.js +10 -5
- package/.out/core/mobx/sub_form_field_adapters.d.ts +2 -2
- package/.out/field_converters/chain_field_converter.js +3 -3
- package/.out/mantine/create_fields_view.d.ts +9 -1
- package/.out/mantine/create_fields_view.js +13 -1
- package/.out/mantine/error_renderer.d.ts +7 -3
- package/.out/mantine/hooks.d.ts +2 -1
- package/.out/mantine/hooks.js +1 -1
- package/.out/mantine/specs/create_fields_view.tests.js +17 -0
- package/.out/mantine/specs/fields_view_hooks.stories.d.ts +6 -2
- package/.out/mantine/specs/fields_view_hooks.stories.js +26 -7
- package/.out/mantine/specs/fields_view_hooks.tests.js +21 -1
- package/.out/tsconfig.tsbuildinfo +1 -1
- package/.out/types/specs/error_of_field.tests.d.ts +1 -0
- package/.out/types/specs/{error_type_of_field.tests.js → error_of_field.tests.js} +1 -1
- package/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-check-types.log +1 -1
- package/core/mobx/field_adapter_builder.ts +71 -0
- package/core/mobx/form_presenter.ts +15 -14
- package/core/mobx/hooks.tsx +196 -0
- package/core/mobx/specs/form_presenter.tests.ts +24 -5
- package/core/mobx/sub_form_field_adapters.ts +14 -3
- package/dist/index.cjs +290 -220
- package/dist/index.d.cts +63 -32
- package/dist/index.d.ts +63 -32
- package/dist/index.js +288 -219
- package/field_converters/chain_field_converter.ts +3 -3
- package/mantine/create_fields_view.tsx +66 -31
- package/mantine/error_renderer.ts +12 -3
- package/mantine/hooks.tsx +9 -6
- package/mantine/specs/__snapshots__/fields_view_hooks.tests.tsx.snap +194 -197
- package/mantine/specs/create_fields_view.tests.ts +29 -0
- package/mantine/specs/fields_view_hooks.stories.tsx +58 -15
- package/mantine/specs/fields_view_hooks.tests.tsx +26 -0
- package/package.json +1 -1
- package/types/specs/{error_type_of_field.tests.ts → error_of_field.tests.ts} +1 -1
- package/core/mobx/hooks.ts +0 -112
- /package/.out/{types/specs/error_type_of_field.tests.d.ts → mantine/specs/create_fields_view.tests.d.ts} +0 -0
|
@@ -7,6 +7,9 @@ declare class FieldAdapterBuilder<From, To, E, ValuePath extends string, Context
|
|
|
7
7
|
constructor(convert: AnnotatedFieldConverter<From, To, ValuePath, Context>, create: FieldValueFactory<From, ValuePath, Context>, revert?: UnreliableFieldConverter<To, From, E, ValuePath, Context> | undefined);
|
|
8
8
|
chain<To2, E2 = E>(converter: AnnotatedFieldConverter<To, To2, ValuePath, Context>, reverter?: UnreliableFieldConverter<To2, To, E2, ValuePath, Context>): FieldAdapterBuilder<From, To2, E | E2, ValuePath, Context>;
|
|
9
9
|
withReverter(reverter: UnreliableFieldConverter<To, From, E, ValuePath, Context>): FieldAdapterBuilder<From, To, E, ValuePath, Context>;
|
|
10
|
+
nullable(): FieldAdapterBuilder<From | null, To | null, E, ValuePath, Context>;
|
|
11
|
+
optional(): FieldAdapterBuilder<From | undefined, To | undefined, E, ValuePath, Context>;
|
|
12
|
+
private or;
|
|
10
13
|
withIdentity(isFrom: (from: To | From) => from is From): FieldAdapterBuilder<From, To | From, E, ValuePath, Context>;
|
|
11
14
|
get narrow(): FieldAdapter<From, To, E, ValuePath, Context>;
|
|
12
15
|
}
|
|
@@ -17,5 +20,6 @@ export declare function adapterFromTwoWayConverter<From, To, E, ValuePath extend
|
|
|
17
20
|
export declare function adapterFromPrototype<From, To, ValuePath extends string, Context>(converter: AnnotatedFieldConverter<From, To, ValuePath, Context>, prototype: From): FieldAdapterBuilder<From, To, never, ValuePath, Context>;
|
|
18
21
|
export declare function adapterFromPrototype<From, To, E, ValuePath extends string, Context>(converter: TwoWayFieldConverter<From, To, E, ValuePath, Context>, prototype: From): FieldAdapterBuilder<From, To, E, ValuePath, Context>;
|
|
19
22
|
export declare function identityAdapter<V, ValuePath extends string, Context>(prototype: V, required?: boolean): FieldAdapterBuilder<V, V, never, ValuePath, Context>;
|
|
23
|
+
export declare function trimmingStringAdapter<ValuePath extends string, Context>(): FieldAdapterBuilder<string, string, never, ValuePath, Context>;
|
|
20
24
|
export declare function listAdapter<E, ValuePath extends string, Context>(): FieldAdapterBuilder<readonly E[], readonly E[], never, ValuePath, Context>;
|
|
21
25
|
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { chainAnnotatedFieldConverter, chainUnreliableFieldConverter, } from 'field_converters/chain_field_converter';
|
|
2
2
|
import { annotatedIdentityConverter, unreliableIdentityConverter, } from 'field_converters/identity_converter';
|
|
3
3
|
import { MaybeIdentityConverter } from 'field_converters/maybe_identity_converter';
|
|
4
|
+
import { TrimmingStringConverter } from 'field_converters/trimming_string_converter';
|
|
4
5
|
import { prototypingFieldValueFactory } from 'field_value_factories/prototyping_field_value_factory';
|
|
6
|
+
import { UnreliableFieldConversionType, } from 'types/field_converters';
|
|
5
7
|
class FieldAdapterBuilder {
|
|
6
8
|
convert;
|
|
7
9
|
create;
|
|
@@ -17,6 +19,32 @@ class FieldAdapterBuilder {
|
|
|
17
19
|
withReverter(reverter) {
|
|
18
20
|
return new FieldAdapterBuilder(this.convert, this.create, reverter);
|
|
19
21
|
}
|
|
22
|
+
nullable() {
|
|
23
|
+
return this.or(null);
|
|
24
|
+
}
|
|
25
|
+
optional() {
|
|
26
|
+
return this.or(undefined);
|
|
27
|
+
}
|
|
28
|
+
or(proto) {
|
|
29
|
+
function isFrom(v) {
|
|
30
|
+
return v !== proto;
|
|
31
|
+
}
|
|
32
|
+
function isTo(v) {
|
|
33
|
+
return v !== proto;
|
|
34
|
+
}
|
|
35
|
+
return new FieldAdapterBuilder((v, valuePath, context) => isFrom(v)
|
|
36
|
+
? this.convert(v, valuePath, context)
|
|
37
|
+
: {
|
|
38
|
+
value: v,
|
|
39
|
+
readonly: false,
|
|
40
|
+
required: false,
|
|
41
|
+
}, this.create, (v, valuePath, context) => isTo(v) && this.revert
|
|
42
|
+
? this.revert(v, valuePath, context)
|
|
43
|
+
: {
|
|
44
|
+
type: UnreliableFieldConversionType.Success,
|
|
45
|
+
value: proto,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
20
48
|
withIdentity(isFrom) {
|
|
21
49
|
const identityConverter = new MaybeIdentityConverter({
|
|
22
50
|
convert: this.convert,
|
|
@@ -46,6 +74,9 @@ export function adapterFromPrototype(converter, prototype) {
|
|
|
46
74
|
export function identityAdapter(prototype, required) {
|
|
47
75
|
return new FieldAdapterBuilder(annotatedIdentityConverter(required), prototypingFieldValueFactory(prototype), unreliableIdentityConverter());
|
|
48
76
|
}
|
|
77
|
+
export function trimmingStringAdapter() {
|
|
78
|
+
return adapterFromTwoWayConverter(new TrimmingStringConverter(), prototypingFieldValueFactory(''));
|
|
79
|
+
}
|
|
49
80
|
export function listAdapter() {
|
|
50
81
|
return new FieldAdapterBuilder(annotatedIdentityConverter(false), prototypingFieldValueFactory([]), unreliableIdentityConverter());
|
|
51
82
|
}
|
|
@@ -20,9 +20,9 @@ type FlattenedErrors<ValuePathsToAdapters extends Readonly<Record<string, FieldA
|
|
|
20
20
|
export type ValuePathsToAdaptersOf<TypePathsToAdapters extends Partial<Readonly<Record<string, FieldAdapter>>>, ValuePathsToTypePaths extends Readonly<Record<string, string>>> = keyof TypePathsToAdapters extends ValueOf<ValuePathsToTypePaths> ? {
|
|
21
21
|
readonly [K in keyof ValuePathsToTypePaths as unknown extends TypePathsToAdapters[ValuePathsToTypePaths[K]] ? never : K]: NonNullable<TypePathsToAdapters[ValuePathsToTypePaths[K]]>;
|
|
22
22
|
} : never;
|
|
23
|
-
export declare class FormPresenter<T extends Type, ValueToTypePaths extends Readonly<Record<string, string>>, TypePathsToAdapters extends FlattenedTypePathsToAdaptersOf<FlattenedValuesOfType<T, '*'>, ValueOfType<ReadonlyTypeOfType<T>>>, ValuePathsToAdapters extends ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths> = ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths>> {
|
|
23
|
+
export declare abstract class FormPresenter<T extends Type, ValueToTypePaths extends Readonly<Record<string, string>>, TypePathsToAdapters extends FlattenedTypePathsToAdaptersOf<FlattenedValuesOfType<T, '*'>, ValueOfType<ReadonlyTypeOfType<T>>>, ValuePathsToAdapters extends ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths> = ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths>> {
|
|
24
24
|
readonly type: T;
|
|
25
|
-
|
|
25
|
+
protected readonly adapters: TypePathsToAdapters;
|
|
26
26
|
constructor(type: T, adapters: TypePathsToAdapters);
|
|
27
27
|
private maybeGetAdapterForValuePath;
|
|
28
28
|
private getAdapterForValuePath;
|
|
@@ -33,12 +33,12 @@ export declare class FormPresenter<T extends Type, ValueToTypePaths extends Read
|
|
|
33
33
|
removeListItem<K extends keyof FlattenedListTypesOfType<T>>(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>, elementValuePath: `${K}.${number}`): void;
|
|
34
34
|
private internalSetFieldValue;
|
|
35
35
|
clearFieldError<K extends keyof ValuePathsToAdapters>(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>, valuePath: K): void;
|
|
36
|
-
clearFieldValue<K extends StringKeyOf<
|
|
36
|
+
clearFieldValue<K extends StringKeyOf<ValueToTypePaths>>(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>, valuePath: K): void;
|
|
37
37
|
clearAll(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>, value: ValueOfType<T>): void;
|
|
38
38
|
isValuePathActive<K extends keyof ValuePathsToAdapters>(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>, valuePath: K): boolean;
|
|
39
|
-
validateField<K extends keyof ValuePathsToAdapters>(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>, valuePath: K): boolean;
|
|
39
|
+
validateField<K extends keyof ValuePathsToAdapters>(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>, valuePath: K, ignoreDefaultValue?: boolean): boolean;
|
|
40
40
|
validateAll(model: FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>): boolean;
|
|
41
|
-
createModel(value: ValueOfType<ReadonlyTypeOfType<T>>): FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>;
|
|
41
|
+
abstract createModel(value: ValueOfType<ReadonlyTypeOfType<T>>): FormModel<T, ValueToTypePaths, TypePathsToAdapters, ValuePathsToAdapters>;
|
|
42
42
|
}
|
|
43
43
|
export declare class FormModel<T extends Type, ValueToTypePaths extends Readonly<Record<string, string>>, TypePathsToAdapters extends FlattenedTypePathsToAdaptersOf<FlattenedValuesOfType<T, '*'>, ValueOfType<ReadonlyTypeOfType<T>>>, ValuePathsToAdapters extends ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths> = ValuePathsToAdaptersOf<TypePathsToAdapters, ValueToTypePaths>> {
|
|
44
44
|
private readonly type;
|
|
@@ -188,8 +188,7 @@ export class FormPresenter {
|
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
190
|
const { convert, create, } = adapter;
|
|
191
|
-
const
|
|
192
|
-
const value = accessor == null ? create(valuePath, model.value) : accessor.value;
|
|
191
|
+
const value = create(valuePath, model.value);
|
|
193
192
|
const { value: displayValue, } = convert(value, valuePath, model.value);
|
|
194
193
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
195
194
|
const key = valuePath;
|
|
@@ -211,7 +210,7 @@ export class FormPresenter {
|
|
|
211
210
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
212
211
|
return keys.has(valuePath);
|
|
213
212
|
}
|
|
214
|
-
validateField(model, valuePath) {
|
|
213
|
+
validateField(model, valuePath, ignoreDefaultValue = false) {
|
|
215
214
|
const { convert, revert, create, } = this.getAdapterForValuePath(valuePath);
|
|
216
215
|
const fieldOverride = model.fieldOverrides[valuePath];
|
|
217
216
|
const accessor = model.getAccessorForValuePath(valuePath);
|
|
@@ -226,6 +225,12 @@ export class FormPresenter {
|
|
|
226
225
|
: storedValue;
|
|
227
226
|
const dirty = storedValue !== value;
|
|
228
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
|
+
}
|
|
229
234
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
230
235
|
const conversion = revert(value, valuePath, model.value);
|
|
231
236
|
return runInAction(function () {
|
|
@@ -293,9 +298,6 @@ export class FormPresenter {
|
|
|
293
298
|
}, true);
|
|
294
299
|
});
|
|
295
300
|
}
|
|
296
|
-
createModel(value) {
|
|
297
|
-
return new FormModel(this.type, value, this.adapters);
|
|
298
|
-
}
|
|
299
301
|
}
|
|
300
302
|
export class FormModel {
|
|
301
303
|
type;
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import { type ReadonlyTypeOfType, type ValueOfType } from '@strictly/define';
|
|
2
2
|
import { type FieldsViewProps } from 'core/props';
|
|
3
|
+
import { type ComponentType } from 'react';
|
|
4
|
+
import { type UnsafePartialComponent } from 'util/partial';
|
|
3
5
|
import { type FormPresenter } from './form_presenter';
|
|
4
|
-
import { type ValuePathsOfPresenter } from './types';
|
|
6
|
+
import { type FormFieldsOfPresenter, type ValuePathsOfPresenter } from './types';
|
|
5
7
|
type ValueOfPresenter<P extends FormPresenter<any, any, any, any>> = P extends FormPresenter<infer T, any, any, any> ? ValueOfType<ReadonlyTypeOfType<T>> : never;
|
|
6
8
|
type ModelOfPresenter<P extends FormPresenter<any, any, any, any>> = ReturnType<P['createModel']>;
|
|
7
|
-
export declare function useDefaultMobxFormHooks<P extends FormPresenter<any, any, any, any>>(presenter: P, value: ValueOfPresenter<P>,
|
|
9
|
+
export declare function useDefaultMobxFormHooks<P extends FormPresenter<any, any, any, any>, C extends ComponentType<FieldsViewProps<F>>, F extends FormFieldsOfPresenter<P> = FormFieldsOfPresenter<P>>(presenter: P, value: ValueOfPresenter<P>, options?: {
|
|
8
10
|
onValidFieldSubmit?: <Path extends ValuePathsOfPresenter<P>>(model: ModelOfPresenter<P>, valuePath: Path) => void;
|
|
9
11
|
onValidFormSubmit?: (model: ModelOfPresenter<P>, value: ValueOfPresenter<P>) => void;
|
|
10
12
|
}): {
|
|
11
13
|
model: ModelOfPresenter<P>;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
FormFields?: UnsafePartialComponent<C, FieldsViewProps<F>>;
|
|
15
|
+
onFormSubmit: () => void;
|
|
16
|
+
onFieldValueChange<K extends keyof F>(this: void, key: K, value: F[K]['value']): void;
|
|
17
|
+
onFieldFocus?(this: void, key: keyof F): void;
|
|
18
|
+
onFieldBlur?(this: void, key: keyof F): void;
|
|
19
|
+
onFieldSubmit?(this: void, key: keyof F): boolean | void;
|
|
20
|
+
};
|
|
21
|
+
export declare function useDefaultMobxFormHooks<P extends FormPresenter<any, any, any, any>, C extends ComponentType<FieldsViewProps<F>>, F extends FormFieldsOfPresenter<P> = FormFieldsOfPresenter<P>>(presenter: P, value: ValueOfPresenter<P>, options: {
|
|
22
|
+
onValidFieldSubmit?: <Path extends ValuePathsOfPresenter<P>>(model: ModelOfPresenter<P>, valuePath: Path) => void;
|
|
23
|
+
onValidFormSubmit?: (model: ModelOfPresenter<P>, value: ValueOfPresenter<P>) => void;
|
|
24
|
+
FormFieldsView: C;
|
|
25
|
+
}): {
|
|
26
|
+
model: ModelOfPresenter<P>;
|
|
27
|
+
FormFields: UnsafePartialComponent<C, FieldsViewProps<F>>;
|
|
28
|
+
onFormSubmit: () => void;
|
|
29
|
+
onFieldValueChange<K extends keyof F>(this: void, key: K, value: F[K]['value']): void;
|
|
30
|
+
onFieldFocus?(this: void, key: keyof F): void;
|
|
31
|
+
onFieldBlur?(this: void, key: keyof F): void;
|
|
32
|
+
onFieldSubmit?(this: void, key: keyof F): boolean | void;
|
|
33
|
+
};
|
|
14
34
|
export {};
|
package/.out/core/mobx/hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, } from 'react';
|
|
2
|
-
|
|
3
|
-
export function useDefaultMobxFormHooks(presenter, value, { onValidFieldSubmit, onValidFormSubmit, }) {
|
|
2
|
+
import { createUnsafePartialObserverComponent, } from 'util/partial';
|
|
3
|
+
export function useDefaultMobxFormHooks(presenter, value, { onValidFieldSubmit, onValidFormSubmit, FormFieldsView, } = {}) {
|
|
4
4
|
const model = useMemo(function () {
|
|
5
5
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
6
6
|
return presenter.createModel(value);
|
|
@@ -31,7 +31,7 @@ export function useDefaultMobxFormHooks(presenter, value, { onValidFieldSubmit,
|
|
|
31
31
|
// TODO debounce?
|
|
32
32
|
setTimeout(function () {
|
|
33
33
|
if (presenter.isValuePathActive(model, path)) {
|
|
34
|
-
presenter.validateField(model, path);
|
|
34
|
+
presenter.validateField(model, path, true);
|
|
35
35
|
}
|
|
36
36
|
}, 100);
|
|
37
37
|
}, [
|
|
@@ -47,11 +47,32 @@ export function useDefaultMobxFormHooks(presenter, value, { onValidFieldSubmit,
|
|
|
47
47
|
model,
|
|
48
48
|
onValidFormSubmit,
|
|
49
49
|
]);
|
|
50
|
+
const FormFields = useMemo(() => {
|
|
51
|
+
if (FormFieldsView == null) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return createUnsafePartialObserverComponent(FormFieldsView, () => {
|
|
55
|
+
return {
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
57
|
+
fields: model.fields,
|
|
58
|
+
onFieldBlur,
|
|
59
|
+
onFieldSubmit,
|
|
60
|
+
onFieldValueChange,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}, [
|
|
64
|
+
model,
|
|
65
|
+
FormFieldsView,
|
|
66
|
+
onFieldBlur,
|
|
67
|
+
onFieldSubmit,
|
|
68
|
+
onFieldValueChange,
|
|
69
|
+
]);
|
|
50
70
|
return {
|
|
51
71
|
model,
|
|
52
72
|
onFieldValueChange,
|
|
53
73
|
onFieldSubmit,
|
|
54
74
|
onFieldBlur,
|
|
55
75
|
onFormSubmit,
|
|
76
|
+
FormFields,
|
|
56
77
|
};
|
|
57
78
|
}
|
|
@@ -9,6 +9,11 @@ import { prototypingFieldValueFactory } from 'field_value_factories/prototyping_
|
|
|
9
9
|
import { UnreliableFieldConversionType, } from 'types/field_converters';
|
|
10
10
|
import { createMockedAdapter, resetMockAdapter, } from './fixtures';
|
|
11
11
|
const IS_NAN_ERROR = 1;
|
|
12
|
+
class TestFormPresenter extends FormPresenter {
|
|
13
|
+
createModel(value) {
|
|
14
|
+
return new FormModel(this.type, value, this.adapters);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
12
17
|
const originalIntegerToStringAdapter = adapterFromTwoWayConverter(new IntegerToStringConverter(IS_NAN_ERROR), prototypingFieldValueFactory(0));
|
|
13
18
|
const originalBooleanToBooleanAdapter = identityAdapter(false);
|
|
14
19
|
describe('all', function () {
|
|
@@ -286,7 +291,7 @@ describe('all', function () {
|
|
|
286
291
|
const adapters = {
|
|
287
292
|
$: integerToStringAdapter,
|
|
288
293
|
};
|
|
289
|
-
const presenter = new
|
|
294
|
+
const presenter = new TestFormPresenter(typeDef, adapters);
|
|
290
295
|
const originalValue = 2;
|
|
291
296
|
let model;
|
|
292
297
|
beforeEach(function () {
|
|
@@ -383,7 +388,7 @@ describe('all', function () {
|
|
|
383
388
|
const converters = {
|
|
384
389
|
'$.*': integerToStringAdapter,
|
|
385
390
|
};
|
|
386
|
-
const presenter = new
|
|
391
|
+
const presenter = new TestFormPresenter(typeDef, converters);
|
|
387
392
|
let originalValue;
|
|
388
393
|
let model;
|
|
389
394
|
beforeEach(function () {
|
|
@@ -661,7 +666,7 @@ describe('all', function () {
|
|
|
661
666
|
$: adapterFromTwoWayConverter(new NullableToBooleanConverter(type, [1], null)),
|
|
662
667
|
'$.*': integerToStringAdapter,
|
|
663
668
|
};
|
|
664
|
-
const presenter = new
|
|
669
|
+
const presenter = new TestFormPresenter(type, adapters);
|
|
665
670
|
let originalValue;
|
|
666
671
|
let model;
|
|
667
672
|
beforeEach(function () {
|
|
@@ -709,7 +714,7 @@ describe('all', function () {
|
|
|
709
714
|
'$.x:a': identityAdapter(0).narrow,
|
|
710
715
|
'$.y:b': identityAdapter(false).narrow,
|
|
711
716
|
};
|
|
712
|
-
const presenter = new
|
|
717
|
+
const presenter = new TestFormPresenter(type, adapters);
|
|
713
718
|
describe('isValuePathActive', function () {
|
|
714
719
|
describe('discriminator x', function () {
|
|
715
720
|
const model = presenter.createModel({
|
|
@@ -766,7 +771,7 @@ describe('all', function () {
|
|
|
766
771
|
$: integerToStringAdapter,
|
|
767
772
|
'$.fake': booleanToBooleanAdapter,
|
|
768
773
|
};
|
|
769
|
-
const presenter = new
|
|
774
|
+
const presenter = new TestFormPresenter(typeDef, converters);
|
|
770
775
|
let originalValue;
|
|
771
776
|
let model;
|
|
772
777
|
beforeEach(function () {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type StringConcatOf } from '@strictly/base';
|
|
2
|
-
import { type Type, type ValueOfType } from '@strictly/define';
|
|
2
|
+
import { type ReadonlyTypeOfType, type Type, type ValueOfType } from '@strictly/define';
|
|
3
3
|
import { type ErrorOfFieldAdapter, type FieldAdapter, type FromOfFieldAdapter, type ToOfFieldAdapter, type ValuePathOfFieldAdapter } from './field_adapter';
|
|
4
4
|
type SubFormFieldAdapter<F extends FieldAdapter, ValuePath extends string, Context> = FieldAdapter<FromOfFieldAdapter<F>, ToOfFieldAdapter<F>, ErrorOfFieldAdapter<F>, ValuePathOfFieldAdapter<F> extends StringConcatOf<'$', infer ValuePathSuffix> ? `${ValuePath}${ValuePathSuffix}` : string, Context>;
|
|
5
5
|
type SubFormFieldAdapters<SubAdapters extends Record<string, FieldAdapter>, TypePath extends string, ValuePath extends string, Context> = {
|
|
6
6
|
[K in keyof SubAdapters as K extends StringConcatOf<'$', infer TypePathSuffix> ? `${TypePath}${TypePathSuffix}` : never]: SubFormFieldAdapter<SubAdapters[K], ValuePath, Context>;
|
|
7
7
|
};
|
|
8
|
-
export declare function subFormFieldAdapters<SubAdapters extends Record<string, FieldAdapter>, TypePath extends string, TypePathsToValuePaths extends Record<TypePath, string>, ContextType extends Type>(subAdapters: SubAdapters, parentTypePath: TypePath, contextType: ContextType): SubFormFieldAdapters<SubAdapters, TypePath, TypePathsToValuePaths[TypePath], ValueOfType<ContextType
|
|
8
|
+
export declare function subFormFieldAdapters<SubAdapters extends Record<string, FieldAdapter>, TypePath extends string, TypePathsToValuePaths extends Record<TypePath, string>, ContextType extends Type>(subAdapters: SubAdapters, parentTypePath: TypePath, contextType: ContextType): SubFormFieldAdapters<SubAdapters, TypePath, TypePathsToValuePaths[TypePath], ValueOfType<ReadonlyTypeOfType<ContextType>>>;
|
|
9
9
|
export {};
|
|
@@ -40,12 +40,12 @@ export function chainUnreliableFieldConverter(from, to) {
|
|
|
40
40
|
}
|
|
41
41
|
export function chainAnnotatedFieldConverter(from, to) {
|
|
42
42
|
return function (value, valuePath, context) {
|
|
43
|
-
const { required: intermediateRequired, readonly:
|
|
44
|
-
const { required: finalRequired, readonly:
|
|
43
|
+
const { required: intermediateRequired, readonly: intermediateReadonly, value: intermediateValue, } = from(value, valuePath, context);
|
|
44
|
+
const { required: finalRequired, readonly: finalReadonly, value: finalValue, } = to(intermediateValue, valuePath, context);
|
|
45
45
|
return {
|
|
46
46
|
value: finalValue,
|
|
47
47
|
required: intermediateRequired || finalRequired,
|
|
48
|
-
readonly:
|
|
48
|
+
readonly: intermediateReadonly || finalReadonly,
|
|
49
49
|
};
|
|
50
50
|
};
|
|
51
51
|
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { type StringConcatOf } from '@strictly/base';
|
|
1
2
|
import type { FieldsViewProps } from 'core/props';
|
|
2
3
|
import type { ComponentType } from 'react';
|
|
3
4
|
import type { AllFieldsOfFields } from 'types/all_fields_of_fields';
|
|
4
5
|
import type { Fields } from 'types/field';
|
|
5
6
|
import type { SubFormFields } from 'types/sub_form_fields';
|
|
6
7
|
import type { MantineFieldComponent } from './types';
|
|
7
|
-
export
|
|
8
|
+
export type CallbackMapper<ValuePath extends string> = {
|
|
9
|
+
<Cb extends (...args: any[]) => any>(cb: Cb): Parameters<Cb> extends [infer SubFormValuePath extends string, ...(infer Rest)] ? SubFormValuePath extends StringConcatOf<ValuePath, infer Postfix> ? (valuePath: `$${Postfix}`, ...rest: Rest) => ReturnType<Cb> : never : never;
|
|
10
|
+
};
|
|
11
|
+
export type FieldsView<ValuePath extends string = string, C extends ComponentType<any> = ComponentType<any>> = {
|
|
12
|
+
Component: C;
|
|
13
|
+
callbackMapper: CallbackMapper<ValuePath>;
|
|
14
|
+
};
|
|
15
|
+
export declare function createFieldsView<F extends Fields, K extends keyof AllFieldsOfFields<F>, P extends FieldsViewProps<Fields> = FieldsViewProps<SubFormFields<F, K>>>(valuePath: K, FieldsView: ComponentType<P>, observableProps: FieldsViewProps<F>): FieldsView<K, MantineFieldComponent<FieldsViewProps<P['fields']>, P, never>>;
|
|
@@ -23,7 +23,7 @@ export function createFieldsView(valuePath, FieldsView, observableProps) {
|
|
|
23
23
|
observableProps.onFieldSubmit?.(toKey(subKey));
|
|
24
24
|
}
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
26
|
-
|
|
26
|
+
const Component = observer(function (props) {
|
|
27
27
|
// convert fields to sub-fields
|
|
28
28
|
const subFields = Object.entries(observableProps.fields).reduce((acc, [fieldKey, fieldValue,]) => {
|
|
29
29
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
@@ -36,4 +36,16 @@ export function createFieldsView(valuePath, FieldsView, observableProps) {
|
|
|
36
36
|
{});
|
|
37
37
|
return (_jsx(FieldsView, { ...props, fields: subFields, onFieldBlur: onFieldBlur, onFieldFocus: onFieldFocus, onFieldSubmit: onFieldSubmit, onFieldValueChange: onFieldValueChange }));
|
|
38
38
|
});
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions
|
|
40
|
+
const callbackMapper = ((callback) => {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
return (subFormValuePath, ...args) => {
|
|
43
|
+
const valuePath = toKey(subFormValuePath);
|
|
44
|
+
return callback(valuePath, ...args);
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
Component,
|
|
49
|
+
callbackMapper,
|
|
50
|
+
};
|
|
39
51
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type ComponentType } from 'react';
|
|
2
|
-
|
|
2
|
+
import { type ErrorOfField } from 'types/error_of_field';
|
|
3
|
+
import { type Fields } from 'types/field';
|
|
4
|
+
type InternalErrorRendererProps<E> = {
|
|
3
5
|
error: E;
|
|
4
6
|
};
|
|
5
|
-
export type
|
|
6
|
-
export
|
|
7
|
+
export type ErrorRendererProps<F extends Fields, K extends keyof Fields> = InternalErrorRendererProps<ErrorOfField<F[K]>>;
|
|
8
|
+
export type ErrorRenderer<E = any> = ComponentType<InternalErrorRendererProps<E>>;
|
|
9
|
+
export declare function DefaultErrorRenderer({ error, }: InternalErrorRendererProps<any>): string;
|
|
10
|
+
export {};
|
package/.out/mantine/hooks.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { type StringFieldsOfFields } from 'types/string_fields_of_fields';
|
|
|
11
11
|
import { type SubFormFields } from 'types/sub_form_fields';
|
|
12
12
|
import { type ValueTypeOfField } from 'types/value_type_of_field';
|
|
13
13
|
import { type SuppliedCheckboxProps } from './create_checkbox';
|
|
14
|
+
import { type FieldsView } from './create_fields_view';
|
|
14
15
|
import { DefaultList, type SuppliedListProps } from './create_list';
|
|
15
16
|
import { type SuppliedPillProps } from './create_pill';
|
|
16
17
|
import { type SuppliedRadioProps } from './create_radio';
|
|
@@ -51,7 +52,7 @@ declare class MantineFormImpl<F extends Fields> implements MantineForm<F> {
|
|
|
51
52
|
pill<K extends keyof AllFieldsOfFields<F>>(valuePath: K): MantineFieldComponent<SuppliedPillProps, PillProps, ErrorOfField<F[K]>>;
|
|
52
53
|
pill<K extends keyof AllFieldsOfFields<F>, P extends SuppliedPillProps>(valuePath: K, Pill: ComponentType<P>): MantineFieldComponent<SuppliedPillProps, P, ErrorOfField<F[K]>>;
|
|
53
54
|
list<K extends keyof ListFieldsOfFields<F>>(valuePath: K): MantineFieldComponent<SuppliedListProps<`${K}.${number}`>, ComponentProps<typeof DefaultList<ElementOfArray<F[K]['value']>, K>>, never>;
|
|
54
|
-
fieldsView<K extends keyof AllFieldsOfFields<F>, P extends FieldsViewProps<Fields> = FieldsViewProps<SubFormFields<F, K>>>(valuePath: K, FieldsView: ComponentType<P>): MantineFieldComponent<FieldsViewProps<P['fields']>, P, never
|
|
55
|
+
fieldsView<K extends keyof AllFieldsOfFields<F>, P extends FieldsViewProps<Fields> = FieldsViewProps<SubFormFields<F, K>>>(valuePath: K, FieldsView: ComponentType<P>): FieldsView<K, MantineFieldComponent<FieldsViewProps<P['fields']>, P, never>>;
|
|
55
56
|
form<K extends keyof AllFieldsOfFields<F>, P extends FormProps<ValueTypeOfField<F[K]>> = FormProps<ValueTypeOfField<F[K]>>>(valuePath: K, Form: ComponentType<P>): MantineFieldComponent<FormProps<ValueTypeOfField<F[K]>>, P, never>;
|
|
56
57
|
}
|
|
57
58
|
export {};
|
package/.out/mantine/hooks.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Cache, } from '@strictly/base';
|
|
|
4
4
|
import { observable, runInAction, } from 'mobx';
|
|
5
5
|
import { useEffect, useMemo, } from 'react';
|
|
6
6
|
import { createCheckbox, } from './create_checkbox';
|
|
7
|
-
import { createFieldsView } from './create_fields_view';
|
|
7
|
+
import { createFieldsView, } from './create_fields_view';
|
|
8
8
|
import { createForm } from './create_form';
|
|
9
9
|
import { createList, DefaultList, } from './create_list';
|
|
10
10
|
import { createPill, } from './create_pill';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
describe('createFieldsView', () => {
|
|
2
|
+
describe('CallbackMapper', () => {
|
|
3
|
+
it('maps a root paths', () => {
|
|
4
|
+
const callbackMapper = null;
|
|
5
|
+
expectTypeOf().toEqualTypeOf();
|
|
6
|
+
});
|
|
7
|
+
it('maps a simple paths', () => {
|
|
8
|
+
const callbackMapper = null;
|
|
9
|
+
expectTypeOf().toEqualTypeOf();
|
|
10
|
+
});
|
|
11
|
+
it('maps a indexed paths', () => {
|
|
12
|
+
const callbackMapper = null;
|
|
13
|
+
expectTypeOf().toEqualTypeOf();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
export {};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
2
2
|
import { type FieldsViewProps } from 'core/props';
|
|
3
3
|
import { type Field } from 'types/field';
|
|
4
|
-
declare function
|
|
4
|
+
export declare function ParentFieldLabel(): string;
|
|
5
|
+
export declare function SubFieldLabel(): string;
|
|
6
|
+
declare function Component({ onClickField: onClickFieldImpl, ...props }: FieldsViewProps<{
|
|
5
7
|
$: Field<string, string>;
|
|
6
8
|
'$.a': Field<string, string>;
|
|
7
|
-
}>
|
|
9
|
+
}> & {
|
|
10
|
+
onClickField: (valuePath: '$' | '$.a') => void;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
12
|
declare const meta: Meta<typeof Component>;
|
|
9
13
|
export default meta;
|
|
10
14
|
type Story = StoryObj<typeof Component>;
|
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { Paper, Stack, Text, } from '@mantine/core';
|
|
3
3
|
import { action } from '@storybook/addon-actions';
|
|
4
4
|
import { useMantineFormFields } from 'mantine/hooks';
|
|
5
|
-
|
|
5
|
+
import { useCallback, useMemo, } from 'react';
|
|
6
|
+
export function ParentFieldLabel() {
|
|
7
|
+
return '$';
|
|
8
|
+
}
|
|
9
|
+
export function SubFieldLabel() {
|
|
10
|
+
return '$ (child)';
|
|
11
|
+
}
|
|
6
12
|
function ErrorRenderer({ error }) {
|
|
7
13
|
return `error ${error}`;
|
|
8
14
|
}
|
|
9
|
-
function SubFieldsView(props) {
|
|
15
|
+
function SubFieldsView({ onClickField: onClickFieldImpl, ...props }) {
|
|
10
16
|
const form = useMantineFormFields(props);
|
|
11
17
|
const TextInput = form.textInput('$');
|
|
12
|
-
|
|
18
|
+
const onClick$ = useCallback(() => {
|
|
19
|
+
onClickFieldImpl('$');
|
|
20
|
+
}, [onClickFieldImpl]);
|
|
21
|
+
return (_jsx(Stack, { children: _jsx(TextInput, { ErrorRenderer: ErrorRenderer, label: SubFieldLabel(), onClick: onClick$ }) }));
|
|
13
22
|
}
|
|
14
|
-
function Component(props) {
|
|
23
|
+
function Component({ onClickField: onClickFieldImpl, ...props }) {
|
|
15
24
|
const form = useMantineFormFields(props);
|
|
16
|
-
const
|
|
25
|
+
const { Component, callbackMapper, } = form.fieldsView('$.a', SubFieldsView);
|
|
17
26
|
const TextInput = form.textInput('$');
|
|
18
|
-
|
|
27
|
+
const onClick$ = useCallback(() => {
|
|
28
|
+
onClickFieldImpl('$');
|
|
29
|
+
}, [onClickFieldImpl]);
|
|
30
|
+
const onClickChildField = useMemo(() => {
|
|
31
|
+
return callbackMapper(onClickFieldImpl);
|
|
32
|
+
}, [
|
|
33
|
+
onClickFieldImpl,
|
|
34
|
+
callbackMapper,
|
|
35
|
+
]);
|
|
36
|
+
return (_jsxs(Stack, { children: [_jsx(TextInput, { ErrorRenderer: ErrorRenderer, label: ParentFieldLabel(), onClick: onClick$ }), _jsxs(Paper, { p: 'sm', withBorder: true, children: [_jsx(Text, { children: "$.a" }), _jsx(Component, { onClickField: onClickChildField })] })] }));
|
|
19
37
|
}
|
|
20
38
|
const meta = {
|
|
21
39
|
component: Component,
|
|
@@ -24,6 +42,7 @@ const meta = {
|
|
|
24
42
|
onFieldFocus: action('onFieldFocus'),
|
|
25
43
|
onFieldSubmit: action('onFieldSubmit'),
|
|
26
44
|
onFieldValueChange: action('onFieldValueChange'),
|
|
45
|
+
onClickField: action('onClickField'),
|
|
27
46
|
},
|
|
28
47
|
};
|
|
29
48
|
export default meta;
|
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { composeStories } from '@storybook/react';
|
|
3
3
|
import { toArray } from '@strictly/base';
|
|
4
|
-
import { render, } from '@testing-library/react';
|
|
4
|
+
import { fireEvent, render, } from '@testing-library/react';
|
|
5
5
|
import * as stories from './fields_view_hooks.stories';
|
|
6
6
|
const composedStories = composeStories(stories);
|
|
7
|
+
const { Empty, } = composedStories;
|
|
7
8
|
describe('field view hooks', function () {
|
|
8
9
|
it.each(toArray(composedStories))('renders %s', function (_name, Story) {
|
|
9
10
|
const wrapper = render(_jsx(Story, {}));
|
|
10
11
|
expect(wrapper.container).toMatchSnapshot();
|
|
11
12
|
});
|
|
13
|
+
describe('callbackMapper', () => {
|
|
14
|
+
it.each([
|
|
15
|
+
[
|
|
16
|
+
'$',
|
|
17
|
+
stories.ParentFieldLabel(),
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
'$.a',
|
|
21
|
+
stories.SubFieldLabel(),
|
|
22
|
+
],
|
|
23
|
+
])('calls back with the correct paths for field at %s', async (valuePath, labelText) => {
|
|
24
|
+
const onClickField = vi.fn();
|
|
25
|
+
const wrapper = render(_jsx(Empty, { onClickField: onClickField }));
|
|
26
|
+
const element = await wrapper.findByLabelText(labelText);
|
|
27
|
+
fireEvent.click(element);
|
|
28
|
+
expect(onClickField).toHaveBeenCalledOnce();
|
|
29
|
+
expect(onClickField).toHaveBeenCalledWith(valuePath);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
12
32
|
});
|