@jayalfredprufrock/mobx-toolbox 0.5.3 → 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/dist/model.d.mts CHANGED
@@ -59,10 +59,12 @@ type StoreThis<M> = {
59
59
  remove(model: M): void;
60
60
  };
61
61
  type InferModel<R, Cfg> = Cfg extends {
62
- transform: (data: any) => infer M;
62
+ transform: (...args: any[]) => infer M;
63
63
  } ? M : R;
64
64
  interface StoreConfig<R> {
65
- transform?: (data: R) => any;
65
+ transform?: (data: R, store: {
66
+ remove(model: any): void;
67
+ }) => any;
66
68
  get?: (...args: any[]) => Promise<R>;
67
69
  getAll?: (...args: any[]) => Promise<R[]>;
68
70
  create?: (...args: any[]) => Promise<R>;
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.mts","names":[],"sources":["../src/model/make-model.ts","../src/model/make-store.ts"],"mappings":"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"}
1
+ {"version":3,"file":"model.d.mts","names":[],"sources":["../src/model/make-model.ts","../src/model/make-store.ts"],"mappings":"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"}
package/dist/model.mjs CHANGED
@@ -72,7 +72,6 @@ function makeModel(schema, config) {
72
72
  proto.delete = async function(...rest) {
73
73
  const params = this.buildParams();
74
74
  const result = params === void 0 ? await del(...rest) : await del(params, ...rest);
75
- console.log("delete", this.store);
76
75
  this.store?.remove?.(this);
77
76
  return result;
78
77
  };
@@ -94,12 +93,12 @@ function makeModel(schema, config) {
94
93
  //#endregion
95
94
  //#region src/model/make-store.ts
96
95
  function makeStore(schema, config) {
97
- const transformFn = config?.transform ?? ((data) => data);
98
96
  class Store {
99
97
  _transform;
100
98
  all;
101
99
  constructor() {
102
- this._transform = transformFn.bind(this);
100
+ const rawTransform = config?.transform;
101
+ this._transform = rawTransform ? (data) => rawTransform.call(this, data, this) : (data) => data;
103
102
  makeObservable(this, {
104
103
  _transform: false,
105
104
  remove: action
@@ -109,7 +108,6 @@ function makeStore(schema, config) {
109
108
  });
110
109
  }
111
110
  remove(model) {
112
- console.log("model", model);
113
111
  this.all?.value?.remove?.(model);
114
112
  }
115
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"model.mjs","names":[],"sources":["../src/model/make-model.ts","../src/model/make-store.ts"],"sourcesContent":["import { action, makeObservable, observable, runInAction, toJS, type AnnotationsMap } from \"mobx\";\nimport * as T from \"typebox\";\n\n// -----------------------------------------------------------------------------\n// Public structural contract\n// -----------------------------------------------------------------------------\n\n/** Minimal interface a model's host store must satisfy. */\nexport interface ModelStore<M> {\n remove?(model: M): void;\n}\n\n// -----------------------------------------------------------------------------\n// Type plumbing\n// -----------------------------------------------------------------------------\n\ntype Resource<S extends T.TObject> = T.Static<S>;\n\ntype KeyShape<S extends T.TObject, K extends readonly (keyof Resource<S>)[]> = K extends readonly []\n ? undefined\n : Pick<Resource<S>, K[number]>;\n\ntype KeyedFn<\n S extends T.TObject,\n K extends readonly (keyof Resource<S>)[],\n R,\n> = K extends readonly []\n ? (...args: any[]) => Promise<R>\n : (params: KeyShape<S, K>, ...rest: any[]) => Promise<R>;\n\ntype KeyedBodyFn<\n S extends T.TObject,\n K extends readonly (keyof Resource<S>)[],\n R,\n> = K extends readonly []\n ? (body: any, ...rest: any[]) => Promise<R>\n : (params: KeyShape<S, K>, body: any, ...rest: any[]) => Promise<R>;\n\n// Strip the first arg when keys is non-empty — model methods don't take the params.\ntype StripParams<K extends readonly any[], F> = K extends readonly []\n ? F\n : F extends (params: any, ...rest: infer R) => infer Ret\n ? (...args: R) => Ret\n : never;\n\n// Replace a function's Promise return with Promise<R>.\ntype ReplaceReturn<F, R> = F extends (...args: infer A) => Promise<any>\n ? (...args: A) => Promise<R>\n : never;\n\ntype ReservedActionKey = \"reload\" | \"update\" | \"delete\" | \"setData\" | \"toJSON\" | \"store\";\n\ntype ActionsConfig<S extends T.TObject, K extends readonly (keyof Resource<S>)[]> = {\n [name: string]: KeyedFn<S, K, Resource<S>>;\n} & { [Key in ReservedActionKey]?: never };\n\nexport interface ModelConfig<S extends T.TObject, K extends readonly (keyof Resource<S>)[]> {\n keys: K;\n reload?: KeyedFn<S, K, Resource<S>>;\n update?: KeyedBodyFn<S, K, Resource<S>>;\n delete?: KeyedFn<S, K, any>;\n actions?: ActionsConfig<S, K>;\n}\n\n// Instance method shape from config. Self-mutating methods return Promise<any>;\n// the instance is mutated in place via setData, so callers typically read fields\n// off the same reference instead of chaining the return.\ntype ModelMethods<K extends readonly any[], Cfg> = (Cfg extends { reload: infer F }\n ? { reload: ReplaceReturn<StripParams<K, F>, any> }\n : {}) &\n (Cfg extends { update: infer F } ? { update: ReplaceReturn<StripParams<K, F>, any> } : {}) &\n (Cfg extends { delete: infer F } ? { delete: StripParams<K, F> } : {}) &\n (Cfg extends { actions: infer A }\n ? {\n [N in keyof A]: A[N] extends (...args: any[]) => any\n ? ReplaceReturn<StripParams<K, A[N]>, any>\n : never;\n }\n : {});\n\n// -----------------------------------------------------------------------------\n// Constructor type\n// -----------------------------------------------------------------------------\n\ntype ModelInstance<S extends T.TObject, K extends readonly any[], Cfg> = Resource<S> & {\n readonly store?: ModelStore<any>;\n setData(data: Partial<Resource<S>>): void;\n toJSON(): Resource<S>;\n buildParams(): KeyShape<S, K extends readonly (keyof Resource<S>)[] ? K : readonly []>;\n getMobxAnnotations?(): AnnotationsMap<any, never>;\n} & ModelMethods<K, Cfg>;\n\nexport type ModelConstructor<S extends T.TObject, K extends readonly any[], Cfg> = {\n new (data: Resource<S>, store?: ModelStore<any>): ModelInstance<S, K, Cfg>;\n readonly schema: S;\n};\n\n// -----------------------------------------------------------------------------\n// makeModel\n// -----------------------------------------------------------------------------\n\nexport function makeModel<S extends T.TObject>(schema: S): ModelConstructor<S, readonly [], {}>;\nexport function makeModel<\n S extends T.TObject,\n K extends readonly (keyof Resource<S>)[],\n Cfg extends ModelConfig<S, K>,\n>(schema: S, config: Cfg): ModelConstructor<S, K, Cfg>;\nexport function makeModel<S extends T.TObject>(\n schema: S,\n config?: ModelConfig<S, readonly (keyof Resource<S>)[]>,\n): any {\n const keys = (config?.keys ?? []) as readonly PropertyKey[];\n\n abstract class BaseModel {\n static readonly schema = schema;\n\n readonly store?: ModelStore<any>;\n\n constructor(data: Resource<S>, store?: ModelStore<any>) {\n this.store = store;\n\n const annotations: Record<string, any> = {};\n for (const key of Object.keys(schema.properties)) {\n Object.defineProperty(this, key, {\n value: (data as any)[key],\n enumerable: true,\n configurable: true,\n writable: true,\n });\n annotations[key] = observable.ref;\n }\n\n makeObservable(this, {\n ...annotations,\n setData: action,\n ...(this as any).getMobxAnnotations?.(),\n });\n }\n\n setData(data: Partial<Resource<S>>): void {\n Object.assign(this, data);\n }\n\n /**\n * Build the params object passed as the first arg to keyed API methods.\n * Default extracts each property in `keys` from the model. Override on a\n * subclass when the model field name differs from the API param name, or\n * to construct composite params from derived values.\n */\n buildParams(): any {\n if (keys.length === 0) return undefined;\n const data = this as any;\n return Object.fromEntries(keys.map((k) => [k, data[k]]));\n }\n\n toJSON(): Resource<S> {\n const data = this as any;\n return Object.keys(schema.properties).reduce(\n (obj, key) => {\n if (data[key] !== undefined) obj[key] = toJS(data[key]);\n return obj;\n },\n {} as Record<string, any>,\n ) as Resource<S>;\n }\n }\n\n const proto = BaseModel.prototype as any;\n\n if (config?.reload) {\n const reload = config.reload as (...args: any[]) => Promise<any>;\n proto.reload = async function (...rest: any[]) {\n const params = this.buildParams();\n const data = params === undefined ? await reload(...rest) : await reload(params, ...rest);\n runInAction(() => this.setData(data));\n return this;\n };\n }\n\n if (config?.update) {\n const update = config.update as (...args: any[]) => Promise<any>;\n proto.update = async function (body: any, ...rest: any[]) {\n const params = this.buildParams();\n const data =\n params === undefined ? await update(body, ...rest) : await update(params, body, ...rest);\n runInAction(() => this.setData(data));\n return this;\n };\n }\n\n if (config?.delete) {\n const del = config.delete as (...args: any[]) => Promise<any>;\n proto.delete = async function (...rest: any[]) {\n const params = this.buildParams();\n const result = params === undefined ? await del(...rest) : await del(params, ...rest);\n console.log(\"delete\", this.store);\n this.store?.remove?.(this);\n return result;\n };\n }\n\n if (config?.actions) {\n for (const [name, fn] of Object.entries(config.actions)) {\n const call = fn as (...args: any[]) => Promise<any>;\n proto[name] = async function (body?: any, ...rest: any[]) {\n const params = this.buildParams();\n let data: any;\n if (params === undefined) {\n data = body === undefined ? await call() : await call(body, ...rest);\n } else {\n data = body === undefined ? await call(params) : await call(params, body, ...rest);\n }\n runInAction(() => this.setData(data));\n return this;\n };\n }\n }\n\n return BaseModel;\n}\n\nexport type { AnnotationsMap };\n","import * as T from \"typebox\";\nimport { lazyObservableArray, type LazyObservableArray } from \"../lazy-observable/lazy-observable\";\nimport { action, makeObservable, runInAction } from \"mobx\";\n\n// -----------------------------------------------------------------------------\n// Type plumbing\n// -----------------------------------------------------------------------------\n\n// Replace a function's Promise return with Promise<R> — used so get/create\n// return models rather than the raw API response.\ntype ReplaceReturn<F, R> = F extends (...args: infer A) => Promise<any>\n ? (...args: A) => Promise<R>\n : never;\n\n// The structural shape passed as `this` inside `transform` — the only member\n// transform ever needs is `remove`, since it's about to construct a model and\n// hand the store reference along to its constructor.\ntype StoreThis<M> = {\n remove(model: M): void;\n};\n\n// Infer M from config: when transform is present use its return type, else use R.\ntype InferModel<R, Cfg> = Cfg extends { transform: (data: any) => infer M } ? M : R;\n\nexport interface StoreConfig<R> {\n transform?: (data: R) => any;\n get?: (...args: any[]) => Promise<R>;\n getAll?: (...args: any[]) => Promise<R[]>;\n create?: (...args: any[]) => Promise<R>;\n}\n\n// Final store-instance shape, derived from config slots.\ntype StoreInstance<M, Cfg> = {\n remove(model: M): void;\n} & (Cfg extends { get: infer G } ? { get: ReplaceReturn<G, M> } : {}) &\n (Cfg extends { getAll: any } ? { getAll: () => Promise<M[]>; all: LazyObservableArray<M> } : {}) &\n (Cfg extends { create: infer C } ? { create: ReplaceReturn<C, M> } : {});\n\nexport type StoreConstructor<M, Cfg> = {\n new (): StoreInstance<M, Cfg>;\n};\n\n// -----------------------------------------------------------------------------\n// makeStore\n// -----------------------------------------------------------------------------\n\nexport function makeStore<S extends T.TObject>(schema: S): StoreConstructor<T.Static<S>, {}>;\nexport function makeStore<S extends T.TObject, Cfg extends StoreConfig<T.Static<S>>>(\n schema: S,\n config: Cfg & ThisType<StoreThis<InferModel<T.Static<S>, Cfg>>>,\n): StoreConstructor<InferModel<T.Static<S>, Cfg>, Cfg>;\nexport function makeStore<S extends T.TObject>(\n schema: S,\n config?: StoreConfig<T.Static<S>>,\n): StoreConstructor<any, any> {\n type R = T.Static<S>;\n\n const transformFn: (data: R) => any = config?.transform ?? ((data) => data);\n\n class Store {\n // Bound `transform` so user-provided `this` keyword resolves to the store instance.\n private readonly _transform: (data: R) => any;\n\n all?: LazyObservableArray<any>;\n\n constructor() {\n this._transform = transformFn.bind(this as any);\n\n makeObservable<this, \"_transform\">(this, {\n _transform: false,\n remove: action,\n });\n\n if (config?.getAll) {\n this.all = lazyObservableArray(async () => {\n const items = await (config.getAll as (...args: any[]) => Promise<R[]>)();\n return items.map((item) => this._transform(item));\n });\n }\n }\n\n remove(model: any): void {\n console.log(\"model\", model);\n this.all?.value?.remove?.(model);\n }\n }\n\n const proto = Store.prototype as any;\n\n if (config?.get) {\n const get = config.get;\n proto.get = function (...args: any[]) {\n return get(...args).then((data: R) => this._transform(data));\n };\n }\n\n if (config?.getAll) {\n proto.getAll = function () {\n return this.all.getOrLoad();\n };\n }\n\n if (config?.create) {\n const create = config.create;\n proto.create = async function (...args: any[]) {\n const data = await create(...args);\n return runInAction(() => {\n const model = this._transform(data);\n if (this.all) this.all.value.unshift(model);\n return model;\n });\n };\n }\n\n return Store as unknown as StoreConstructor<any, any>;\n}\n\nexport type { LazyObservableArray };\n"],"mappings":"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"}
1
+ {"version":3,"file":"model.mjs","names":[],"sources":["../src/model/make-model.ts","../src/model/make-store.ts"],"sourcesContent":["import { action, makeObservable, observable, runInAction, toJS, type AnnotationsMap } from \"mobx\";\nimport * as T from \"typebox\";\n\n// -----------------------------------------------------------------------------\n// Public structural contract\n// -----------------------------------------------------------------------------\n\n/** Minimal interface a model's host store must satisfy. */\nexport interface ModelStore<M> {\n remove?(model: M): void;\n}\n\n// -----------------------------------------------------------------------------\n// Type plumbing\n// -----------------------------------------------------------------------------\n\ntype Resource<S extends T.TObject> = T.Static<S>;\n\ntype KeyShape<S extends T.TObject, K extends readonly (keyof Resource<S>)[]> = K extends readonly []\n ? undefined\n : Pick<Resource<S>, K[number]>;\n\ntype KeyedFn<\n S extends T.TObject,\n K extends readonly (keyof Resource<S>)[],\n R,\n> = K extends readonly []\n ? (...args: any[]) => Promise<R>\n : (params: KeyShape<S, K>, ...rest: any[]) => Promise<R>;\n\ntype KeyedBodyFn<\n S extends T.TObject,\n K extends readonly (keyof Resource<S>)[],\n R,\n> = K extends readonly []\n ? (body: any, ...rest: any[]) => Promise<R>\n : (params: KeyShape<S, K>, body: any, ...rest: any[]) => Promise<R>;\n\n// Strip the first arg when keys is non-empty — model methods don't take the params.\ntype StripParams<K extends readonly any[], F> = K extends readonly []\n ? F\n : F extends (params: any, ...rest: infer R) => infer Ret\n ? (...args: R) => Ret\n : never;\n\n// Replace a function's Promise return with Promise<R>.\ntype ReplaceReturn<F, R> = F extends (...args: infer A) => Promise<any>\n ? (...args: A) => Promise<R>\n : never;\n\ntype ReservedActionKey = \"reload\" | \"update\" | \"delete\" | \"setData\" | \"toJSON\" | \"store\";\n\ntype ActionsConfig<S extends T.TObject, K extends readonly (keyof Resource<S>)[]> = {\n [name: string]: KeyedFn<S, K, Resource<S>>;\n} & { [Key in ReservedActionKey]?: never };\n\nexport interface ModelConfig<S extends T.TObject, K extends readonly (keyof Resource<S>)[]> {\n keys: K;\n reload?: KeyedFn<S, K, Resource<S>>;\n update?: KeyedBodyFn<S, K, Resource<S>>;\n delete?: KeyedFn<S, K, any>;\n actions?: ActionsConfig<S, K>;\n}\n\n// Instance method shape from config. Self-mutating methods return Promise<any>;\n// the instance is mutated in place via setData, so callers typically read fields\n// off the same reference instead of chaining the return.\ntype ModelMethods<K extends readonly any[], Cfg> = (Cfg extends { reload: infer F }\n ? { reload: ReplaceReturn<StripParams<K, F>, any> }\n : {}) &\n (Cfg extends { update: infer F } ? { update: ReplaceReturn<StripParams<K, F>, any> } : {}) &\n (Cfg extends { delete: infer F } ? { delete: StripParams<K, F> } : {}) &\n (Cfg extends { actions: infer A }\n ? {\n [N in keyof A]: A[N] extends (...args: any[]) => any\n ? ReplaceReturn<StripParams<K, A[N]>, any>\n : never;\n }\n : {});\n\n// -----------------------------------------------------------------------------\n// Constructor type\n// -----------------------------------------------------------------------------\n\ntype ModelInstance<S extends T.TObject, K extends readonly any[], Cfg> = Resource<S> & {\n readonly store?: ModelStore<any>;\n setData(data: Partial<Resource<S>>): void;\n toJSON(): Resource<S>;\n buildParams(): KeyShape<S, K extends readonly (keyof Resource<S>)[] ? K : readonly []>;\n getMobxAnnotations?(): AnnotationsMap<any, never>;\n} & ModelMethods<K, Cfg>;\n\nexport type ModelConstructor<S extends T.TObject, K extends readonly any[], Cfg> = {\n new (data: Resource<S>, store?: ModelStore<any>): ModelInstance<S, K, Cfg>;\n readonly schema: S;\n};\n\n// -----------------------------------------------------------------------------\n// makeModel\n// -----------------------------------------------------------------------------\n\nexport function makeModel<S extends T.TObject>(schema: S): ModelConstructor<S, readonly [], {}>;\nexport function makeModel<\n S extends T.TObject,\n K extends readonly (keyof Resource<S>)[],\n Cfg extends ModelConfig<S, K>,\n>(schema: S, config: Cfg): ModelConstructor<S, K, Cfg>;\nexport function makeModel<S extends T.TObject>(\n schema: S,\n config?: ModelConfig<S, readonly (keyof Resource<S>)[]>,\n): any {\n const keys = (config?.keys ?? []) as readonly PropertyKey[];\n\n abstract class BaseModel {\n static readonly schema = schema;\n\n readonly store?: ModelStore<any>;\n\n constructor(data: Resource<S>, store?: ModelStore<any>) {\n this.store = store;\n\n const annotations: Record<string, any> = {};\n for (const key of Object.keys(schema.properties)) {\n Object.defineProperty(this, key, {\n value: (data as any)[key],\n enumerable: true,\n configurable: true,\n writable: true,\n });\n annotations[key] = observable.ref;\n }\n\n makeObservable(this, {\n ...annotations,\n setData: action,\n ...(this as any).getMobxAnnotations?.(),\n });\n }\n\n setData(data: Partial<Resource<S>>): void {\n Object.assign(this, data);\n }\n\n /**\n * Build the params object passed as the first arg to keyed API methods.\n * Default extracts each property in `keys` from the model. Override on a\n * subclass when the model field name differs from the API param name, or\n * to construct composite params from derived values.\n */\n buildParams(): any {\n if (keys.length === 0) return undefined;\n const data = this as any;\n return Object.fromEntries(keys.map((k) => [k, data[k]]));\n }\n\n toJSON(): Resource<S> {\n const data = this as any;\n return Object.keys(schema.properties).reduce(\n (obj, key) => {\n if (data[key] !== undefined) obj[key] = toJS(data[key]);\n return obj;\n },\n {} as Record<string, any>,\n ) as Resource<S>;\n }\n }\n\n const proto = BaseModel.prototype as any;\n\n if (config?.reload) {\n const reload = config.reload as (...args: any[]) => Promise<any>;\n proto.reload = async function (...rest: any[]) {\n const params = this.buildParams();\n const data = params === undefined ? await reload(...rest) : await reload(params, ...rest);\n runInAction(() => this.setData(data));\n return this;\n };\n }\n\n if (config?.update) {\n const update = config.update as (...args: any[]) => Promise<any>;\n proto.update = async function (body: any, ...rest: any[]) {\n const params = this.buildParams();\n const data =\n params === undefined ? await update(body, ...rest) : await update(params, body, ...rest);\n runInAction(() => this.setData(data));\n return this;\n };\n }\n\n if (config?.delete) {\n const del = config.delete as (...args: any[]) => Promise<any>;\n proto.delete = async function (...rest: any[]) {\n const params = this.buildParams();\n const result = params === undefined ? await del(...rest) : await del(params, ...rest);\n this.store?.remove?.(this);\n return result;\n };\n }\n\n if (config?.actions) {\n for (const [name, fn] of Object.entries(config.actions)) {\n const call = fn as (...args: any[]) => Promise<any>;\n proto[name] = async function (body?: any, ...rest: any[]) {\n const params = this.buildParams();\n let data: any;\n if (params === undefined) {\n data = body === undefined ? await call() : await call(body, ...rest);\n } else {\n data = body === undefined ? await call(params) : await call(params, body, ...rest);\n }\n runInAction(() => this.setData(data));\n return this;\n };\n }\n }\n\n return BaseModel;\n}\n\nexport type { AnnotationsMap };\n","import * as T from \"typebox\";\nimport { lazyObservableArray, type LazyObservableArray } from \"../lazy-observable/lazy-observable\";\nimport { action, makeObservable, runInAction } from \"mobx\";\n\n// -----------------------------------------------------------------------------\n// Type plumbing\n// -----------------------------------------------------------------------------\n\n// Replace a function's Promise return with Promise<R> — used so get/create\n// return models rather than the raw API response.\ntype ReplaceReturn<F, R> = F extends (...args: infer A) => Promise<any>\n ? (...args: A) => Promise<R>\n : never;\n\n// The structural shape passed as `this` inside `transform` — the only member\n// transform ever needs is `remove`, since it's about to construct a model and\n// hand the store reference along to its constructor.\ntype StoreThis<M> = {\n remove(model: M): void;\n};\n\n// Infer M from config: when transform is present use its return type, else use R.\ntype InferModel<R, Cfg> = Cfg extends { transform: (...args: any[]) => infer M } ? M : R;\n\nexport interface StoreConfig<R> {\n transform?: (data: R, store: { remove(model: any): void }) => any;\n get?: (...args: any[]) => Promise<R>;\n getAll?: (...args: any[]) => Promise<R[]>;\n create?: (...args: any[]) => Promise<R>;\n}\n\n// Final store-instance shape, derived from config slots.\ntype StoreInstance<M, Cfg> = {\n remove(model: M): void;\n} & (Cfg extends { get: infer G } ? { get: ReplaceReturn<G, M> } : {}) &\n (Cfg extends { getAll: any } ? { getAll: () => Promise<M[]>; all: LazyObservableArray<M> } : {}) &\n (Cfg extends { create: infer C } ? { create: ReplaceReturn<C, M> } : {});\n\nexport type StoreConstructor<M, Cfg> = {\n new (): StoreInstance<M, Cfg>;\n};\n\n// -----------------------------------------------------------------------------\n// makeStore\n// -----------------------------------------------------------------------------\n\nexport function makeStore<S extends T.TObject>(schema: S): StoreConstructor<T.Static<S>, {}>;\nexport function makeStore<S extends T.TObject, Cfg extends StoreConfig<T.Static<S>>>(\n schema: S,\n config: Cfg & ThisType<StoreThis<InferModel<T.Static<S>, Cfg>>>,\n): StoreConstructor<InferModel<T.Static<S>, Cfg>, Cfg>;\nexport function makeStore<S extends T.TObject>(\n schema: S,\n config?: StoreConfig<T.Static<S>>,\n): StoreConstructor<any, any> {\n type R = T.Static<S>;\n\n class Store {\n private readonly _transform: (data: R) => any;\n\n all?: LazyObservableArray<any>;\n\n constructor() {\n const rawTransform = config?.transform;\n this._transform = rawTransform\n ? (data: R) => rawTransform.call(this, data, this)\n : (data: R) => data as any;\n\n makeObservable<this, \"_transform\">(this, {\n _transform: false,\n remove: action,\n });\n\n if (config?.getAll) {\n this.all = lazyObservableArray(async () => {\n const items = await (config.getAll as (...args: any[]) => Promise<R[]>)();\n return items.map((item) => this._transform(item));\n });\n }\n }\n\n remove(model: any): void {\n this.all?.value?.remove?.(model);\n }\n }\n\n const proto = Store.prototype as any;\n\n if (config?.get) {\n const get = config.get;\n proto.get = function (...args: any[]) {\n return get(...args).then((data: R) => this._transform(data));\n };\n }\n\n if (config?.getAll) {\n proto.getAll = function () {\n return this.all.getOrLoad();\n };\n }\n\n if (config?.create) {\n const create = config.create;\n proto.create = async function (...args: any[]) {\n const data = await create(...args);\n return runInAction(() => {\n const model = this._transform(data);\n if (this.all) this.all.value.unshift(model);\n return model;\n });\n };\n }\n\n return Store as unknown as StoreConstructor<any, any>;\n}\n\nexport type { LazyObservableArray };\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.3",
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": {