@jayalfredprufrock/mobx-toolbox 0.5.4 → 0.6.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.
package/dist/form.d.mts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as _$react from "react";
2
- import * as _$typebox from "typebox";
3
- import { Static, TObject, TSchema } from "typebox";
2
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
4
3
  import { Validator } from "typebox/schema";
4
+ import * as _$typebox from "typebox";
5
+ import { Static, TLiteral, TObject, TSchema, TUnion } from "typebox";
5
6
 
6
7
  //#region src/form/form-field.model.d.ts
7
8
  declare class FormFieldModel<T extends T.TSchema = T.TSchema> {
@@ -22,22 +23,51 @@ declare class FormFieldModel<T extends T.TSchema = T.TSchema> {
22
23
  }
23
24
  //#endregion
24
25
  //#region src/form/form.types.d.ts
25
- interface FormConfig<T extends TObject = TObject> {
26
+ type FormSchema = TObject | TUnion;
27
+ interface FormConfig<T extends FormSchema = TObject> {
26
28
  handleSubmit: (data: Static<T>) => Promise<unknown>;
27
- initialValues?: Partial<Static<T>>;
29
+ initialValues?: FormInitialValues<T>;
28
30
  }
29
31
  interface FormFieldConfig<T extends TSchema = TSchema> {
30
32
  name: string;
31
33
  schema: T;
32
34
  initialValue?: Static<T>;
33
35
  }
34
- type FormFields<T extends TObject = TObject> = { [K in keyof T["properties"]]: FormFieldModel<T["properties"][K]> };
36
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
37
+ /** Keys present on the variant `O` (a single union member). */
38
+ type VariantKeys<O> = O extends TObject ? keyof O["properties"] : never;
39
+ /** Keys present on *every* variant — the shared base + discriminator. */
40
+ type SharedKeys<O> = UnionToIntersection<O extends TObject ? {
41
+ k: keyof O["properties"];
42
+ } : never> extends {
43
+ k: infer K extends PropertyKey;
44
+ } ? K : never;
45
+ /** The schema of key `K` across the variants that declare it (unioned when they differ). */
46
+ type VariantSchema<O, K extends PropertyKey> = O extends TObject ? K extends keyof O["properties"] ? O["properties"][K] : never : never;
47
+ /** The static value type of key `K` across the variants that declare it. */
48
+ type VariantValue<O, K extends PropertyKey> = O extends TObject ? K extends keyof O["properties"] ? Static<O["properties"][K]> : never : never;
49
+ type FormFields<T extends FormSchema = TObject> = T extends TUnion<infer V> ? { [K in SharedKeys<V[number]>]: FormFieldModel<VariantSchema<V[number], K>> } : T extends TObject ? { [K in keyof T["properties"]]: FormFieldModel<T["properties"][K]> } : never;
50
+ type RawFormFields<T extends FormSchema = TObject> = T extends TUnion<infer V> ? { [K in VariantKeys<V[number]>]: FormFieldModel<VariantSchema<V[number], K>> } : T extends TObject ? { [K in keyof T["properties"]]: FormFieldModel<T["properties"][K]> } : never;
51
+ type FormInitialValues<T extends FormSchema> = T extends TUnion<infer V> ? { [K in VariantKeys<V[number]>]?: VariantValue<V[number], K> } : T extends TObject ? Partial<Static<T>> : never;
52
+ /** Keys that are typed as a literal in at least one variant — valid discriminators. */
53
+ type DiscriminatorKeys<T extends TUnion> = T extends TUnion<infer V> ? LiteralKeys<V[number]> : never;
54
+ type LiteralKeys<O> = O extends TObject ? { [K in keyof O["properties"]]: O["properties"][K] extends TLiteral ? K : never }[keyof O["properties"]] : never;
55
+ /** The literal values a given discriminator key can take across all variants. */
56
+ type DiscriminatorValue<T extends TUnion, D extends PropertyKey> = T extends TUnion<infer V> ? LiteralValue<V[number], D> : never;
57
+ type LiteralValue<O, D extends PropertyKey> = O extends TObject ? D extends keyof O["properties"] ? O["properties"][D] extends TLiteral<infer L> ? L : never : never : never;
58
+ /** The variant object whose discriminator `D` equals the literal `V`. */
59
+ type MatchVariant<T extends TUnion, D extends PropertyKey, V> = T extends TUnion<infer Vars> ? Distribute<Vars[number], D, V> : never;
60
+ type Distribute<O, D extends PropertyKey, V> = O extends TObject ? D extends keyof O["properties"] ? O["properties"][D] extends TLiteral<infer L> ? V extends L ? O : never : never : never : never;
35
61
  //#endregion
36
62
  //#region src/form/form.model.d.ts
37
- declare class FormModel<T extends TObject = TObject> {
63
+ declare class FormModel<T extends FormSchema = TObject> {
64
+ /** Shared fields only (for unions); every field for a plain object schema. */
38
65
  readonly fields: FormFields<T>;
66
+ /** Every field across all variants — escape hatch for reaching variant fields. */
67
+ readonly rawFields: RawFormFields<T>;
39
68
  readonly config: FormConfig<T>;
40
69
  readonly schema: T;
70
+ readonly validator: Validator<T>;
41
71
  submitting: boolean;
42
72
  submitted: boolean;
43
73
  submitError: any;
@@ -58,6 +88,28 @@ interface MobxFormProps extends Omit<React.HTMLProps<HTMLFormElement>, "form" |
58
88
  }
59
89
  declare const MobxForm: _$react.ForwardRefExoticComponent<Omit<MobxFormProps, "ref"> & _$react.RefAttributes<unknown>>;
60
90
  //#endregion
91
+ //#region src/form/components/form-when.d.ts
92
+ interface FormWhenProps<T extends TUnion, D extends DiscriminatorKeys<T>, V extends string> {
93
+ form: FormModel<T>;
94
+ /** The discriminator field name. */
95
+ field: D;
96
+ /** Render the children only while the discriminator field equals this value. */
97
+ value: V & DiscriminatorValue<T, D>;
98
+ /** Receives the matching variant's fields. The form itself is already in scope. */
99
+ children: (fields: FormFields<MatchVariant<T, D, V>>) => React.ReactNode;
100
+ }
101
+ /**
102
+ * Renders its children only when the form's discriminator field currently holds
103
+ * `value`, passing the fields narrowed to that variant. Stack one per variant to
104
+ * lay out a discriminated-union form without manual conditionals or casts.
105
+ */
106
+ declare function FormWhen<T extends TUnion, D extends DiscriminatorKeys<T>, V extends string>({
107
+ form,
108
+ field,
109
+ value,
110
+ children
111
+ }: FormWhenProps<T, D, V>): _$react_jsx_runtime0.JSX.Element;
112
+ //#endregion
61
113
  //#region src/form/form.context.d.ts
62
114
  declare const formContext: _$react.Context<FormModel<_$typebox.TObject<_$typebox.TProperties>> | undefined>;
63
115
  declare const useFormContext: () => FormModel<_$typebox.TObject<_$typebox.TProperties>>;
@@ -65,7 +117,7 @@ declare const FormProvider: _$react.Provider<FormModel<_$typebox.TObject<_$typeb
65
117
  declare const useFormContextIfAvailable: () => FormModel<_$typebox.TObject<_$typebox.TProperties>> | undefined;
66
118
  //#endregion
67
119
  //#region src/form/use-form.d.ts
68
- declare const useForm: <T extends TObject = TObject>(schema: T, config: FormConfig<T>) => FormModel<T>;
120
+ declare const useForm: <T extends FormSchema = TObject>(schema: T, config: FormConfig<T>) => FormModel<T>;
69
121
  //#endregion
70
- export { FormConfig, FormFieldConfig, FormFieldModel, FormFields, FormModel, FormProvider, MobxForm, MobxFormProps, formContext, useForm, useFormContext, useFormContextIfAvailable };
122
+ export { DiscriminatorKeys, DiscriminatorValue, FormConfig, FormFieldConfig, FormFieldModel, FormFields, FormModel, FormProvider, FormSchema, FormWhen, FormWhenProps, MatchVariant, MobxForm, MobxFormProps, RawFormFields, formContext, useForm, useFormContext, useFormContextIfAvailable };
71
123
  //# sourceMappingURL=form.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"form.d.mts","names":[],"sources":["../src/form/form-field.model.ts","../src/form/form.types.ts","../src/form/form.model.ts","../src/form/components/form.tsx","../src/form/form.context.ts","../src/form/use-form.ts"],"mappings":";;;;;;cAUa,cAAA,WAAyB,CAAA,CAAE,OAAA,GAAU,CAAA,CAAE,OAAA;EAAA,SACzC,IAAA;EAAA,SACA,MAAA,EAAQ,CAAA;EAAA,SACR,MAAA,EAAQ,eAAA,CAAgB,CAAA;EAAA,SACxB,SAAA,EAAW,SAAA,CAAU,CAAA;EAE9B,KAAA,EAAO,CAAA,CAAE,MAAA,CAAO,CAAA;EAChB,OAAA;EAAA,IAEI,KAAA,CAAA;EAAA,IAKA,YAAA,CAAA;EA2BJ,WAAA,CAAY,MAAA,EAAQ,eAAA,CAAgB,CAAA;EAepC,QAAA,CAAS,KAAA,GAAQ,CAAA,CAAE,MAAA,CAAO,CAAA;EAM1B,UAAA,CAAW,OAAA;EAIX,KAAA,CAAA;EAQA,KAAA,CAAA;EAWA,MAAA,CAAA,GAAU,CAAA,CAAE,MAAA,CAAO,CAAA;AAAA;;;UC5FJ,UAAA,WAAqB,OAAA,GAAU,OAAA;EAC9C,YAAA,GAAe,IAAA,EAAM,MAAA,CAAO,CAAA,MAAO,OAAA;EACnC,aAAA,GAAgB,OAAA,CAAQ,MAAA,CAAO,CAAA;AAAA;AAAA,UAGhB,eAAA,WAA0B,OAAA,GAAU,OAAA;EACnD,IAAA;EACA,MAAA,EAAQ,CAAA;EACR,YAAA,GAAe,MAAA,CAAO,CAAA;AAAA;AAAA,KAGZ,UAAA,WAAqB,OAAA,GAAU,OAAA,kBAC7B,CAAA,iBAAkB,cAAA,CAAe,CAAA,eAAgB,CAAA;;;cCGlD,SAAA,WAAoB,OAAA,GAAU,OAAA;EAAA,SAChC,MAAA,EAAQ,UAAA,CAAW,CAAA;EAAA,SACnB,MAAA,EAAQ,UAAA,CAAW,CAAA;EAAA,SACnB,MAAA,EAAQ,CAAA;EAGjB,UAAA;EACA,SAAA;EAGA,WAAA;EAEA,WAAA,CAAY,MAAA,EAAQ,CAAA,EAAG,MAAA,EAAQ,UAAA,CAAW,CAAA;EAAA,IAmBtC,KAAA,CAAA;EAIJ,KAAA,CAAA;EA4BA,cAAA,CAAe,KAAA;EAAA,UAIL,aAAA,CAAc,UAAA;EAAA,UAId,YAAA,CAAa,SAAA;EAIvB,KAAA,CAAA;EAOA,QAAA,CAAA;EAOA,MAAA,CAAA,GAAU,OAAA,CAAQ,MAAA,CAAO,CAAA;AAAA;;;UCvGV,aAAA,SAAsB,IAAA,CACrC,KAAA,CAAM,SAAA,CAAU,eAAA;EAGhB,IAAA,EAAM,SAAA;AAAA;AAAA,cAGK,QAAA,EAAQ,OAAA,CAAA,yBAAA,CAAA,IAAA,CAAA,aAAA,WAAA,OAAA,CAAA,aAAA;;;cCRR,WAAA,EAAW,OAAA,CAAA,OAAA,CAAA,SAAA,CAAA,SAAA,CAAA,OAAA,CAAkD,SAAA,CAAlD,WAAA;AAAA,cACX,cAAA,QAAc,SAAA,CAAA,SAAA,CAAA,OAAA,CAQ1B,SAAA,CAR0B,WAAA;AAAA,cAUd,YAAA,EAAY,OAAA,CAAA,QAAA,CAAA,SAAA,CAAA,SAAA,CAAA,OAAA,CAAuB,SAAA,CAAvB,WAAA;AAAA,cAEZ,yBAAA,QAAyB,SAAA,CAAA,SAAA,CAAA,OAAA,CAAgC,SAAA,CAAhC,WAAA;;;cCXzB,OAAA,aAAqB,OAAA,GAAU,OAAA,EAAO,MAAA,EACzC,CAAA,EAAC,MAAA,EACD,UAAA,CAAW,CAAA,MAClB,SAAA,CAAU,CAAA"}
1
+ {"version":3,"file":"form.d.mts","names":[],"sources":["../src/form/form-field.model.ts","../src/form/form.types.ts","../src/form/form.model.ts","../src/form/components/form.tsx","../src/form/components/form-when.tsx","../src/form/form.context.ts","../src/form/use-form.ts"],"mappings":"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"}
package/dist/form.mjs CHANGED
@@ -1,10 +1,12 @@
1
+ import { Observer } from "mobx-react-lite";
1
2
  import { createContext, forwardRef, useContext, useRef } from "react";
2
3
  import { makeAutoObservable, toJS } from "mobx";
3
- import { jsx } from "react/jsx-runtime";
4
+ import { Fragment, jsx } from "react/jsx-runtime";
4
5
  import * as Format from "typebox/format";
5
- import * as T from "typebox";
6
- import Schema, { Validator } from "typebox/schema";
7
6
  import * as Value from "typebox/value";
7
+ import Schema, { Validator } from "typebox/schema";
8
+ import * as T from "typebox";
9
+ import { IsUnion, Union } from "typebox";
8
10
 
9
11
  //#region src/form/form.context.ts
10
12
  const formContext = createContext(void 0);
@@ -31,6 +33,21 @@ const MobxForm = forwardRef(function MobxForm({ form, children, ...formProps },
31
33
  });
32
34
  });
33
35
 
36
+ //#endregion
37
+ //#region src/form/components/form-when.tsx
38
+ /**
39
+ * Renders its children only when the form's discriminator field currently holds
40
+ * `value`, passing the fields narrowed to that variant. Stack one per variant to
41
+ * lay out a discriminated-union form without manual conditionals or casts.
42
+ */
43
+ function FormWhen({ form, field, value, children }) {
44
+ return /* @__PURE__ */ jsx(Observer, { children: () => {
45
+ const fields = form.rawFields;
46
+ if (fields[field]?.value !== value) return null;
47
+ return /* @__PURE__ */ jsx(Fragment, { children: children(fields) });
48
+ } });
49
+ }
50
+
34
51
  //#endregion
35
52
  //#region src/form/form-field.model.ts
36
53
  var FormFieldModel = class {
@@ -99,31 +116,53 @@ var FormFieldModel = class {
99
116
  //#region src/form/form.model.ts
100
117
  Format.Set("password", () => true);
101
118
  Format.Set("phone", () => true);
119
+ function resolveProperties(schema) {
120
+ if (!IsUnion(schema)) return schema.properties;
121
+ const groups = {};
122
+ for (const variant of schema.anyOf) for (const [key, propSchema] of Object.entries(variant.properties)) (groups[key] ??= []).push(propSchema);
123
+ const merged = {};
124
+ for (const [key, schemas] of Object.entries(groups)) {
125
+ const distinct = [...new Map(schemas.map((s) => [JSON.stringify(s), s])).values()];
126
+ merged[key] = distinct.length === 1 ? distinct[0] : Union(distinct);
127
+ }
128
+ return merged;
129
+ }
102
130
  var FormModel = class {
131
+ /** Shared fields only (for unions); every field for a plain object schema. */
103
132
  fields;
133
+ /** Every field across all variants — escape hatch for reaching variant fields. */
134
+ rawFields;
104
135
  config;
105
136
  schema;
137
+ validator;
106
138
  submitting = false;
107
139
  submitted = false;
108
140
  submitError;
109
141
  constructor(schema, config) {
110
142
  this.schema = schema;
111
143
  this.config = config;
144
+ this.validator = Schema.Compile(schema);
112
145
  makeAutoObservable(this, {
113
146
  fields: false,
147
+ rawFields: false,
114
148
  schema: false,
115
- config: false
149
+ config: false,
150
+ validator: false
116
151
  });
117
- this.fields = Object.entries(schema.properties).reduce((fields, [fieldName, fieldSchema]) => {
152
+ const initialValues = config?.initialValues;
153
+ const fields = Object.entries(resolveProperties(schema)).reduce((fields, [fieldName, fieldSchema]) => {
118
154
  fields[fieldName] = new FormFieldModel({
119
155
  name: fieldName,
120
156
  schema: fieldSchema,
121
- initialValue: config?.initialValues?.[fieldName]
157
+ initialValue: initialValues?.[fieldName]
122
158
  });
123
159
  return fields;
124
160
  }, {});
161
+ this.rawFields = fields;
162
+ this.fields = fields;
125
163
  }
126
164
  get valid() {
165
+ if (IsUnion(this.schema)) return this.validator.Check(this.toJSON());
127
166
  return Object.values(this.fields).every((field) => field.valid);
128
167
  }
129
168
  props() {
@@ -160,10 +199,12 @@ var FormModel = class {
160
199
  return this.valid;
161
200
  }
162
201
  toJSON() {
163
- return Object.values(this.fields).reduce((fields, field) => {
164
- fields[field.name] = field.value;
202
+ const data = Object.values(this.fields).reduce((fields, field) => {
203
+ fields[field.name] = field.toJSON();
165
204
  return fields;
166
205
  }, {});
206
+ if (IsUnion(this.schema)) return Value.Clean(this.schema, data);
207
+ return data;
167
208
  }
168
209
  };
169
210
 
@@ -177,5 +218,5 @@ const useForm = (schema, config) => {
177
218
  };
178
219
 
179
220
  //#endregion
180
- export { FormFieldModel, FormModel, FormProvider, MobxForm, formContext, useForm, useFormContext, useFormContextIfAvailable };
221
+ export { FormFieldModel, FormModel, FormProvider, FormWhen, MobxForm, formContext, useForm, useFormContext, useFormContextIfAvailable };
181
222
  //# sourceMappingURL=form.mjs.map
package/dist/form.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"form.mjs","names":[],"sources":["../src/form/form.context.ts","../src/form/components/form.tsx","../src/form/form-field.model.ts","../src/form/form.model.ts","../src/form/use-form.ts"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { FormModel } from \"./form.model\";\n\nexport const formContext = createContext<FormModel | undefined>(undefined);\nexport const useFormContext = () => {\n const context = useContext(formContext);\n if (!context) {\n throw new Error(\n \"Form context not available. Make sure you are within the <Form> component or providing the form context manually.\",\n );\n }\n return context;\n};\n\nexport const FormProvider = formContext.Provider;\n\nexport const useFormContextIfAvailable = () => useContext(formContext);\n","import { forwardRef } from \"react\";\nimport { FormProvider } from \"../form.context\";\nimport type { FormModel } from \"../form.model\";\n\nexport interface MobxFormProps extends Omit<\n React.HTMLProps<HTMLFormElement>,\n \"form\" | \"action\" | \"method\"\n> {\n form: FormModel<any>;\n}\n\nexport const MobxForm = forwardRef(function MobxForm(\n { form, children, ...formProps }: MobxFormProps,\n ref,\n) {\n return (\n <FormProvider value={form}>\n <form noValidate {...formProps} ref={ref} {...form.props()}>\n {children}\n </form>\n </FormProvider>\n );\n});\n","import Schema, { Validator } from \"typebox/schema\";\nimport * as Value from \"typebox/value\";\nimport * as T from \"typebox\";\nimport { makeAutoObservable, toJS } from \"mobx\";\nimport type { FormFieldConfig } from \"./form.types\";\n\n// TODO: should infer required prop\n// TODO: should also allow for \"id\", defaulting to name\n// TODO: probably shouldn't assume value is of correct type, maybe unknown?\n\nexport class FormFieldModel<T extends T.TSchema = T.TSchema> {\n readonly name: string;\n readonly schema: T;\n readonly config: FormFieldConfig<T>;\n readonly validator: Validator<T>;\n\n value: T.Static<T> | undefined;\n touched = false;\n\n get valid(): boolean {\n if (this.value === undefined && T.IsOptional(this.schema)) return true;\n return this.validator.Check(this.value);\n }\n\n get errorMessage(): string {\n if (this.valid || !this.touched) return \"\";\n // TODO: revisit this, now that .Errors returns success/fail\n const [_, errors] = this.validator.Errors(this.value);\n const error = errors.at(0);\n\n if (!error) return \"\";\n\n if (!T.IsOptional(this.schema) && (this.value === undefined || this.value === \"\")) {\n return \"This field is required.\";\n }\n\n if (\"errorMessage\" in this.schema) {\n if (typeof this.schema.errorMessage === \"string\") {\n return this.schema.errorMessage;\n } else if (typeof this.schema.errorMessage === \"function\") {\n return this.schema.errorMessage(this.value, this.schema);\n }\n }\n\n if (T.IsString(this.schema) && \"format\" in this.schema) {\n return `Please enter a valid ${String(this.schema.format)}`;\n }\n\n return error.message;\n }\n\n constructor(config: FormFieldConfig<T>) {\n this.config = config;\n this.name = config.name;\n this.schema = config.schema;\n this.validator = Schema.Compile(this.schema);\n this.value = Value.Convert(this.schema, config.initialValue) as T.Static<T>;\n\n makeAutoObservable(this, {\n name: false,\n schema: false,\n config: false,\n validator: false,\n });\n }\n\n setValue(value?: T.Static<T>) {\n // TODO: does this still make sense? If value is invalid,\n // then type will be wrong...revisit this\n this.value = Value.Convert(this.schema, value) as T.Static<T>;\n }\n\n setTouched(touched: boolean) {\n this.touched = touched;\n }\n\n reset() {\n this.setTouched(false);\n this.setValue(this.config.initialValue);\n }\n\n // TODO: this any type is dangerous, need to figure out a good way\n // to make this work OTTB for most form controls, while allowing\n // some kind of escape hatch for special cases\n props(): any {\n return {\n name: this.name,\n onChange: (v?: T.Static<T>) => this.setValue(v),\n value: this.value,\n onBlur: () => {\n this.setTouched(true);\n },\n };\n }\n\n toJSON(): T.Static<T> | undefined {\n return toJS(this.value);\n }\n}\n","import * as Format from \"typebox/format\";\nimport { type Static, type TObject, type TSchema } from \"typebox\";\nimport { makeAutoObservable } from \"mobx\";\nimport type { FormConfig, FormFields } from \"./form.types\";\nimport { FormFieldModel } from \"./form-field.model\";\n\n// should these be here?\nFormat.Set(\"password\", () => true);\nFormat.Set(\"phone\", () => true);\n\n// consider using enumerable to allow spreading of\n// field model instead of calling props() unless we need\n// to pass data to props...\n\n// TODO:\n// handleSubmit should catch a special MobxFormError\n// that can set field-level errors from an API response\n\nexport class FormModel<T extends TObject = TObject> {\n readonly fields: FormFields<T>;\n readonly config: FormConfig<T>;\n readonly schema: T;\n\n // TODO: maybe refactor into a \"state\" property?\n submitting = false;\n submitted = false;\n\n // TODO: what should this type be? should it be generic?\n submitError: any;\n\n constructor(schema: T, config: FormConfig<T>) {\n this.schema = schema;\n this.config = config;\n\n makeAutoObservable(this, { fields: false, schema: false, config: false });\n\n this.fields = Object.entries(schema.properties).reduce(\n (fields, [fieldName, fieldSchema]) => {\n fields[fieldName] = new FormFieldModel({\n name: fieldName,\n schema: fieldSchema,\n initialValue: config?.initialValues?.[fieldName],\n });\n return fields;\n },\n {} as Record<string, FormFieldModel<TSchema>>,\n ) as FormFields<T>;\n }\n\n get valid(): boolean {\n return Object.values(this.fields).every((field) => field.valid);\n }\n\n props(): any {\n return {\n onSubmit: (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n\n this.setSubmitError(undefined);\n\n if (!this.validate()) {\n return;\n }\n\n this.setSubmitting(true);\n this.config\n .handleSubmit(this.toJSON() as Static<T>)\n .then((resp) => {\n this.setSubmitted(true);\n return resp;\n })\n .catch((e) => {\n this.setSubmitError(e);\n })\n .finally(() => {\n this.setSubmitting(false);\n });\n },\n };\n }\n\n setSubmitError(error: any): void {\n this.submitError = error;\n }\n\n protected setSubmitting(submitting: boolean): void {\n this.submitting = submitting;\n }\n\n protected setSubmitted(submitted: boolean): void {\n this.submitted = submitted;\n }\n\n reset(): void {\n this.setSubmitError(undefined);\n for (const field of Object.values(this.fields)) {\n field.reset();\n }\n }\n\n validate(): boolean {\n for (const field of Object.values(this.fields)) {\n field.setTouched(true);\n }\n return this.valid;\n }\n\n toJSON(): Partial<Static<T>> {\n return Object.values(this.fields).reduce(\n (fields, field) => {\n fields[field.name] = field.value;\n return fields;\n },\n {} as Partial<Static<T>>,\n );\n }\n}\n","import type { TObject } from \"typebox\";\nimport { useRef } from \"react\";\nimport { FormModel } from \"./form.model\";\nimport type { FormConfig } from \"./form.types\";\n\nexport const useForm = <T extends TObject = TObject>(\n schema: T,\n config: FormConfig<T>,\n): FormModel<T> => {\n const formRef = useRef<FormModel<T>>(undefined);\n if (formRef.current) {\n Object.assign(formRef.current.config, config);\n } else {\n formRef.current = new FormModel<T>(schema, config);\n }\n\n return formRef.current;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"form.mjs","names":[],"sources":["../src/form/form.context.ts","../src/form/components/form.tsx","../src/form/components/form-when.tsx","../src/form/form-field.model.ts","../src/form/form.model.ts","../src/form/use-form.ts"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { FormModel } from \"./form.model\";\n\nexport const formContext = createContext<FormModel | undefined>(undefined);\nexport const useFormContext = () => {\n const context = useContext(formContext);\n if (!context) {\n throw new Error(\n \"Form context not available. Make sure you are within the <Form> component or providing the form context manually.\",\n );\n }\n return context;\n};\n\nexport const FormProvider = formContext.Provider;\n\nexport const useFormContextIfAvailable = () => useContext(formContext);\n","import { forwardRef } from \"react\";\nimport { FormProvider } from \"../form.context\";\nimport type { FormModel } from \"../form.model\";\n\nexport interface MobxFormProps extends Omit<\n React.HTMLProps<HTMLFormElement>,\n \"form\" | \"action\" | \"method\"\n> {\n form: FormModel<any>;\n}\n\nexport const MobxForm = forwardRef(function MobxForm(\n { form, children, ...formProps }: MobxFormProps,\n ref,\n) {\n return (\n <FormProvider value={form}>\n <form noValidate {...formProps} ref={ref} {...form.props()}>\n {children}\n </form>\n </FormProvider>\n );\n});\n","import type { TUnion } from \"typebox\";\nimport { Observer } from \"mobx-react-lite\";\nimport type { FormModel } from \"../form.model\";\nimport type { FormFieldModel } from \"../form-field.model\";\nimport type {\n DiscriminatorKeys,\n DiscriminatorValue,\n FormFields,\n MatchVariant,\n} from \"../form.types\";\n\nexport interface FormWhenProps<T extends TUnion, D extends DiscriminatorKeys<T>, V extends string> {\n form: FormModel<T>;\n /** The discriminator field name. */\n field: D;\n /** Render the children only while the discriminator field equals this value. */\n value: V & DiscriminatorValue<T, D>;\n /** Receives the matching variant's fields. The form itself is already in scope. */\n children: (fields: FormFields<MatchVariant<T, D, V>>) => React.ReactNode;\n}\n\n/**\n * Renders its children only when the form's discriminator field currently holds\n * `value`, passing the fields narrowed to that variant. Stack one per variant to\n * lay out a discriminated-union form without manual conditionals or casts.\n */\nexport function FormWhen<T extends TUnion, D extends DiscriminatorKeys<T>, V extends string>({\n form,\n field,\n value,\n children,\n}: FormWhenProps<T, D, V>) {\n return (\n <Observer>\n {() => {\n const fields = form.rawFields as Record<string, FormFieldModel>;\n if (fields[field as string]?.value !== value) return null;\n return <>{children(fields as unknown as FormFields<MatchVariant<T, D, V>>)}</>;\n }}\n </Observer>\n );\n}\n","import Schema, { Validator } from \"typebox/schema\";\nimport * as Value from \"typebox/value\";\nimport * as T from \"typebox\";\nimport { makeAutoObservable, toJS } from \"mobx\";\nimport type { FormFieldConfig } from \"./form.types\";\n\n// TODO: should infer required prop\n// TODO: should also allow for \"id\", defaulting to name\n// TODO: probably shouldn't assume value is of correct type, maybe unknown?\n\nexport class FormFieldModel<T extends T.TSchema = T.TSchema> {\n readonly name: string;\n readonly schema: T;\n readonly config: FormFieldConfig<T>;\n readonly validator: Validator<T>;\n\n value: T.Static<T> | undefined;\n touched = false;\n\n get valid(): boolean {\n if (this.value === undefined && T.IsOptional(this.schema)) return true;\n return this.validator.Check(this.value);\n }\n\n get errorMessage(): string {\n if (this.valid || !this.touched) return \"\";\n // TODO: revisit this, now that .Errors returns success/fail\n const [_, errors] = this.validator.Errors(this.value);\n const error = errors.at(0);\n\n if (!error) return \"\";\n\n if (!T.IsOptional(this.schema) && (this.value === undefined || this.value === \"\")) {\n return \"This field is required.\";\n }\n\n if (\"errorMessage\" in this.schema) {\n if (typeof this.schema.errorMessage === \"string\") {\n return this.schema.errorMessage;\n } else if (typeof this.schema.errorMessage === \"function\") {\n return this.schema.errorMessage(this.value, this.schema);\n }\n }\n\n if (T.IsString(this.schema) && \"format\" in this.schema) {\n return `Please enter a valid ${String(this.schema.format)}`;\n }\n\n return error.message;\n }\n\n constructor(config: FormFieldConfig<T>) {\n this.config = config;\n this.name = config.name;\n this.schema = config.schema;\n this.validator = Schema.Compile(this.schema);\n this.value = Value.Convert(this.schema, config.initialValue) as T.Static<T>;\n\n makeAutoObservable(this, {\n name: false,\n schema: false,\n config: false,\n validator: false,\n });\n }\n\n setValue(value?: T.Static<T>) {\n // TODO: does this still make sense? If value is invalid,\n // then type will be wrong...revisit this\n this.value = Value.Convert(this.schema, value) as T.Static<T>;\n }\n\n setTouched(touched: boolean) {\n this.touched = touched;\n }\n\n reset() {\n this.setTouched(false);\n this.setValue(this.config.initialValue);\n }\n\n // TODO: this any type is dangerous, need to figure out a good way\n // to make this work OTTB for most form controls, while allowing\n // some kind of escape hatch for special cases\n props(): any {\n return {\n name: this.name,\n onChange: (v?: T.Static<T>) => this.setValue(v),\n value: this.value,\n onBlur: () => {\n this.setTouched(true);\n },\n };\n }\n\n toJSON(): T.Static<T> | undefined {\n return toJS(this.value);\n }\n}\n","import * as Format from \"typebox/format\";\nimport * as Value from \"typebox/value\";\nimport Schema, { type Validator } from \"typebox/schema\";\nimport { IsUnion, Union, type Static, type TObject, type TSchema } from \"typebox\";\nimport { makeAutoObservable } from \"mobx\";\nimport type { FormConfig, FormFields, FormSchema, RawFormFields } from \"./form.types\";\nimport { FormFieldModel } from \"./form-field.model\";\n\n// should these be here?\nFormat.Set(\"password\", () => true);\nFormat.Set(\"phone\", () => true);\n\n// Flatten a schema's fields into a single property map. For a discriminated\n// union this merges the properties of every variant, unioning the schemas of\n// any field that appears in more than one variant (which naturally turns the\n// discriminator into a union of its literals).\nfunction resolveProperties(schema: FormSchema): Record<string, TSchema> {\n if (!IsUnion(schema)) return schema.properties;\n\n const groups: Record<string, TSchema[]> = {};\n for (const variant of schema.anyOf) {\n for (const [key, propSchema] of Object.entries((variant as TObject).properties)) {\n (groups[key] ??= []).push(propSchema as TSchema);\n }\n }\n\n const merged: Record<string, TSchema> = {};\n for (const [key, schemas] of Object.entries(groups)) {\n const distinct = [...new Map(schemas.map((s) => [JSON.stringify(s), s])).values()];\n merged[key] = distinct.length === 1 ? distinct[0]! : Union(distinct);\n }\n return merged;\n}\n\n// consider using enumerable to allow spreading of\n// field model instead of calling props() unless we need\n// to pass data to props...\n\n// TODO:\n// handleSubmit should catch a special MobxFormError\n// that can set field-level errors from an API response\n\nexport class FormModel<T extends FormSchema = TObject> {\n /** Shared fields only (for unions); every field for a plain object schema. */\n readonly fields: FormFields<T>;\n /** Every field across all variants — escape hatch for reaching variant fields. */\n readonly rawFields: RawFormFields<T>;\n readonly config: FormConfig<T>;\n readonly schema: T;\n readonly validator: Validator<T>;\n\n // TODO: maybe refactor into a \"state\" property?\n submitting = false;\n submitted = false;\n\n // TODO: what should this type be? should it be generic?\n submitError: any;\n\n constructor(schema: T, config: FormConfig<T>) {\n this.schema = schema;\n this.config = config;\n this.validator = Schema.Compile(schema);\n\n makeAutoObservable(this, {\n fields: false,\n rawFields: false,\n schema: false,\n config: false,\n validator: false,\n });\n\n const initialValues = config?.initialValues as Record<string, unknown> | undefined;\n const fields = Object.entries(resolveProperties(schema)).reduce(\n (fields, [fieldName, fieldSchema]) => {\n fields[fieldName] = new FormFieldModel({\n name: fieldName,\n schema: fieldSchema,\n initialValue: initialValues?.[fieldName],\n });\n return fields;\n },\n {} as Record<string, FormFieldModel<TSchema>>,\n );\n\n // `fields` and `rawFields` are the same object; the types differ so that a\n // union form only surfaces shared fields by default.\n this.rawFields = fields as unknown as RawFormFields<T>;\n this.fields = fields as unknown as FormFields<T>;\n }\n\n get valid(): boolean {\n // A union form can't be validated field-by-field — fields belonging to the\n // inactive variant would fail — so validate the assembled object instead.\n if (IsUnion(this.schema)) {\n return this.validator.Check(this.toJSON());\n }\n return Object.values(this.fields).every((field) => field.valid);\n }\n\n props(): any {\n return {\n onSubmit: (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n\n this.setSubmitError(undefined);\n\n if (!this.validate()) {\n return;\n }\n\n this.setSubmitting(true);\n this.config\n .handleSubmit(this.toJSON() as Static<T>)\n .then((resp) => {\n this.setSubmitted(true);\n return resp;\n })\n .catch((e) => {\n this.setSubmitError(e);\n })\n .finally(() => {\n this.setSubmitting(false);\n });\n },\n };\n }\n\n setSubmitError(error: any): void {\n this.submitError = error;\n }\n\n protected setSubmitting(submitting: boolean): void {\n this.submitting = submitting;\n }\n\n protected setSubmitted(submitted: boolean): void {\n this.submitted = submitted;\n }\n\n reset(): void {\n this.setSubmitError(undefined);\n for (const field of Object.values(this.fields)) {\n field.reset();\n }\n }\n\n validate(): boolean {\n for (const field of Object.values(this.fields)) {\n field.setTouched(true);\n }\n return this.valid;\n }\n\n toJSON(): Partial<Static<T>> {\n const data = Object.values(this.fields).reduce(\n (fields, field) => {\n fields[field.name] = field.toJSON();\n return fields;\n },\n {} as Record<string, unknown>,\n );\n\n // Drop fields belonging to the inactive variant so submitted data matches\n // the selected member of the union exactly.\n if (IsUnion(this.schema)) {\n return Value.Clean(this.schema, data) as Partial<Static<T>>;\n }\n return data as Partial<Static<T>>;\n }\n}\n","import type { TObject } from \"typebox\";\nimport { useRef } from \"react\";\nimport { FormModel } from \"./form.model\";\nimport type { FormConfig, FormSchema } from \"./form.types\";\n\nexport const useForm = <T extends FormSchema = TObject>(\n schema: T,\n config: FormConfig<T>,\n): FormModel<T> => {\n const formRef = useRef<FormModel<T>>(undefined);\n if (formRef.current) {\n Object.assign(formRef.current.config, config);\n } else {\n formRef.current = new FormModel<T>(schema, config);\n }\n\n return formRef.current;\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jayalfredprufrock/mobx-toolbox",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Misc mobx/react modules/utilities.",
5
5
  "homepage": "https://github.com/jayalfredprufrock/mobx-toolbox#readme",
6
6
  "bugs": {