@jayalfredprufrock/mobx-toolbox 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dialog.mjs +127 -1
- package/dist/dialog.mjs.map +1 -1
- package/dist/form.mjs +172 -1
- package/dist/form.mjs.map +1 -1
- package/dist/react-util.mjs +151 -1
- package/dist/react-util.mjs.map +1 -1
- package/dist/router.d.mts +4 -4
- package/dist/router.d.mts.map +1 -1
- package/dist/router.mjs +388 -1
- package/dist/router.mjs.map +1 -1
- package/dist/util.mjs +145 -1
- package/dist/util.mjs.map +1 -1
- package/package.json +1 -3
package/dist/dialog.mjs
CHANGED
|
@@ -1,2 +1,128 @@
|
|
|
1
|
-
import{observer
|
|
1
|
+
import { observer } from "mobx-react-lite";
|
|
2
|
+
import { createContext, useContext, useRef } from "react";
|
|
3
|
+
import { makeAutoObservable } from "mobx";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/dialog/dialog.context.ts
|
|
7
|
+
const dialogStoreContext = createContext(void 0);
|
|
8
|
+
const useDialogStore = () => {
|
|
9
|
+
const context = useContext(dialogStoreContext);
|
|
10
|
+
if (!context) throw new Error("Dialog store context not available. Are you using the <Dialogs /> component?");
|
|
11
|
+
return context;
|
|
12
|
+
};
|
|
13
|
+
const DialogStoreProvider = dialogStoreContext.Provider;
|
|
14
|
+
const dialogContext = createContext(void 0);
|
|
15
|
+
const useDialogContext = () => {
|
|
16
|
+
const context = useContext(dialogContext);
|
|
17
|
+
if (!context) throw new Error("Dialog context not available. Are you using <DialogProvider /> ?");
|
|
18
|
+
return context;
|
|
19
|
+
};
|
|
20
|
+
const useDialogContextIfAvailable = () => useContext(dialogContext);
|
|
21
|
+
const DialogProvider = dialogContext.Provider;
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/dialog/dialog.model.ts
|
|
25
|
+
var DialogModel = class {
|
|
26
|
+
id;
|
|
27
|
+
component;
|
|
28
|
+
props;
|
|
29
|
+
openedAt = 0;
|
|
30
|
+
previouslyFocusedElement = null;
|
|
31
|
+
state;
|
|
32
|
+
constructor(store, config) {
|
|
33
|
+
this.store = store;
|
|
34
|
+
this.config = config;
|
|
35
|
+
this.id = config.id ?? btoa(Math.random().toString()).substring(3, 12);
|
|
36
|
+
this.component = config.component;
|
|
37
|
+
this.props = config.props;
|
|
38
|
+
this.setState(config.initialState ?? "closed");
|
|
39
|
+
makeAutoObservable(this, {
|
|
40
|
+
id: false,
|
|
41
|
+
component: false,
|
|
42
|
+
props: false,
|
|
43
|
+
openedAt: false,
|
|
44
|
+
previouslyFocusedElement: false
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
close(closeImmediately) {
|
|
48
|
+
this.setState(closeImmediately ? "closed" : "closing");
|
|
49
|
+
}
|
|
50
|
+
open(openImmediately) {
|
|
51
|
+
this.setState(openImmediately ? "opened" : "opening");
|
|
52
|
+
}
|
|
53
|
+
setState(state) {
|
|
54
|
+
if (this.state === state) return;
|
|
55
|
+
this.state = state;
|
|
56
|
+
if (state === "opened" || state === "opening") {
|
|
57
|
+
this.openedAt = Date.now();
|
|
58
|
+
if (state === "opening") this.previouslyFocusedElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
59
|
+
} else if (state === "closed") {
|
|
60
|
+
this.previouslyFocusedElement?.focus();
|
|
61
|
+
if (this.config.removeOnClosed) this.store.dialogs.delete(this.id);
|
|
62
|
+
this.previouslyFocusedElement = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/dialog/dialog.store.ts
|
|
69
|
+
var DialogStore = class {
|
|
70
|
+
dialogs = /* @__PURE__ */ new Map();
|
|
71
|
+
get openDialogs() {
|
|
72
|
+
return [...this.dialogs.values()].filter((d) => d.state !== "closed").sort((d1, d2) => d1.openedAt - d2.openedAt);
|
|
73
|
+
}
|
|
74
|
+
get activeDialog() {
|
|
75
|
+
return this.openDialogs.at(-1);
|
|
76
|
+
}
|
|
77
|
+
constructor() {
|
|
78
|
+
makeAutoObservable(this);
|
|
79
|
+
}
|
|
80
|
+
add(config) {
|
|
81
|
+
const dialogModel = new DialogModel(this, config);
|
|
82
|
+
this.dialogs.set(dialogModel.id, dialogModel);
|
|
83
|
+
return dialogModel;
|
|
84
|
+
}
|
|
85
|
+
open(...args) {
|
|
86
|
+
const [component, props] = args;
|
|
87
|
+
return this.add({
|
|
88
|
+
component,
|
|
89
|
+
props,
|
|
90
|
+
removeOnClosed: true,
|
|
91
|
+
initialState: "opening"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
close(closeImmediately) {
|
|
95
|
+
this.activeDialog?.close(closeImmediately);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/dialog/use-dialogs.ts
|
|
101
|
+
const useDialogs = (store) => {
|
|
102
|
+
const storeRef = useRef(store);
|
|
103
|
+
if (!storeRef.current) storeRef.current = new DialogStore();
|
|
104
|
+
return storeRef.current;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/dialog/components/dialogs.tsx
|
|
109
|
+
const MobxDialogs = observer(({ store }) => {
|
|
110
|
+
const dialogStore = useDialogs(store);
|
|
111
|
+
return /* @__PURE__ */ jsx(DialogStoreProvider, {
|
|
112
|
+
value: dialogStore,
|
|
113
|
+
children: dialogStore.openDialogs.map((dialog) => {
|
|
114
|
+
return /* @__PURE__ */ jsx(MobxDialog, { dialog }, dialog.id);
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
const MobxDialog = observer(({ dialog }) => {
|
|
119
|
+
const { component: DialogComponent, props: dialogProps } = dialog;
|
|
120
|
+
return /* @__PURE__ */ jsx(DialogProvider, {
|
|
121
|
+
value: dialog,
|
|
122
|
+
children: /* @__PURE__ */ jsx(DialogComponent, { ...dialogProps })
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
export { DialogModel, DialogProvider, DialogStore, DialogStoreProvider, MobxDialog, MobxDialogs, dialogContext, dialogStoreContext, useDialogContext, useDialogContextIfAvailable, useDialogStore, useDialogs };
|
|
2
128
|
//# sourceMappingURL=dialog.mjs.map
|
package/dist/dialog.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialog.mjs","names":[],"sources":["../src/dialog/dialog.context.ts","../src/dialog/dialog.model.ts","../src/dialog/dialog.store.ts","../src/dialog/use-dialogs.ts","../src/dialog/components/dialogs.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { DialogModel } from \"./dialog.model\";\nimport type { DialogStore } from \"./dialog.store\";\n\nexport const dialogStoreContext = createContext<DialogStore | undefined>(undefined);\nexport const useDialogStore = () => {\n const context = useContext(dialogStoreContext);\n if (!context) {\n throw new Error(\"Dialog store context not available. Are you using the <Dialogs /> component?\");\n }\n return context;\n};\n\nexport const DialogStoreProvider = dialogStoreContext.Provider;\n\nexport const dialogContext = createContext<DialogModel | undefined>(undefined);\nexport const useDialogContext = () => {\n const context = useContext(dialogContext);\n if (!context) {\n throw new Error(\"Dialog context not available. Are you using <DialogProvider /> ?\");\n }\n return context;\n};\n\nexport const useDialogContextIfAvailable = () => useContext(dialogContext);\n\nexport const DialogProvider = dialogContext.Provider;\n","import { makeAutoObservable } from \"mobx\";\nimport type { DialogStore } from \"./dialog.store\";\nimport type { AnyComponent, DialogModelConfig, DialogState } from \"./dialog.types\";\n\nexport class DialogModel {\n readonly id: string;\n readonly component: AnyComponent;\n readonly props?: React.ComponentProps<AnyComponent>;\n\n openedAt = 0;\n previouslyFocusedElement: HTMLElement | null = null;\n\n state!: DialogState;\n\n constructor(\n readonly store: DialogStore,\n readonly config: DialogModelConfig,\n ) {\n this.id = config.id ?? btoa(Math.random().toString()).substring(3, 12);\n this.component = config.component;\n this.props = config.props;\n\n this.setState(config.initialState ?? \"closed\");\n\n makeAutoObservable(this, {\n id: false,\n component: false,\n props: false,\n openedAt: false,\n previouslyFocusedElement: false,\n });\n }\n\n close(closeImmediately?: boolean): void {\n this.setState(closeImmediately ? \"closed\" : \"closing\");\n }\n\n open(openImmediately?: boolean): void {\n this.setState(openImmediately ? \"opened\" : \"opening\");\n }\n\n setState(state: DialogState): void {\n if (this.state === state) return;\n\n this.state = state;\n if (state === \"opened\" || state === \"opening\") {\n this.openedAt = Date.now();\n if (state === \"opening\") {\n this.previouslyFocusedElement =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n }\n } else if (state === \"closed\") {\n this.previouslyFocusedElement?.focus();\n if (this.config.removeOnClosed) {\n this.store.dialogs.delete(this.id);\n }\n this.previouslyFocusedElement = null;\n }\n }\n}\n","import { makeAutoObservable } from \"mobx\";\nimport { DialogModel } from \"./dialog.model\";\nimport type { AnyComponent, DialogComponentAndProps, DialogModelConfig } from \"./dialog.types\";\n\nexport class DialogStore {\n dialogs = new Map<string, DialogModel>();\n\n get openDialogs(): DialogModel[] {\n return [...this.dialogs.values()]\n .filter((d) => d.state !== \"closed\")\n .sort((d1, d2) => d1.openedAt - d2.openedAt);\n }\n\n get activeDialog(): DialogModel | undefined {\n return this.openDialogs.at(-1);\n }\n\n constructor() {\n makeAutoObservable(this);\n }\n\n add(config: DialogModelConfig): DialogModel {\n const dialogModel = new DialogModel(this, config);\n this.dialogs.set(dialogModel.id, dialogModel);\n return dialogModel;\n }\n\n open<C extends AnyComponent>(...args: DialogComponentAndProps<C>): DialogModel {\n const [component, props] = args;\n return this.add({\n component,\n props,\n removeOnClosed: true,\n initialState: \"opening\",\n });\n }\n\n close(closeImmediately?: boolean): void {\n this.activeDialog?.close(closeImmediately);\n }\n}\n","import { useRef } from \"react\";\nimport { DialogStore } from \"./dialog.store\";\n\nexport const useDialogs = (store?: DialogStore): DialogStore => {\n const storeRef = useRef<DialogStore | undefined>(store);\n if (!storeRef.current) {\n storeRef.current = new DialogStore();\n }\n return storeRef.current;\n};\n","import { observer } from \"mobx-react-lite\";\nimport { DialogProvider, DialogStoreProvider } from \"../dialog.context\";\nimport type { DialogModel } from \"../dialog.model\";\nimport type { DialogStore } from \"../dialog.store\";\nimport { useDialogs } from \"../use-dialogs\";\n\nexport interface MobxDialogsProps {\n store?: DialogStore;\n}\n\nexport const MobxDialogs = observer(({ store }: MobxDialogsProps) => {\n const dialogStore = useDialogs(store);\n\n return (\n <DialogStoreProvider value={dialogStore}>\n {dialogStore.openDialogs.map((dialog) => {\n return <MobxDialog key={dialog.id} dialog={dialog} />;\n })}\n </DialogStoreProvider>\n );\n});\n\nexport interface MobxDialogProps {\n dialog: DialogModel;\n}\n\nexport const MobxDialog = observer(({ dialog }: MobxDialogProps) => {\n const { component: DialogComponent, props: dialogProps } = dialog;\n return (\n <DialogProvider value={dialog}>\n <DialogComponent {...dialogProps} />\n </DialogProvider>\n );\n});\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"dialog.mjs","names":[],"sources":["../src/dialog/dialog.context.ts","../src/dialog/dialog.model.ts","../src/dialog/dialog.store.ts","../src/dialog/use-dialogs.ts","../src/dialog/components/dialogs.tsx"],"sourcesContent":["import { createContext, useContext } from \"react\";\nimport type { DialogModel } from \"./dialog.model\";\nimport type { DialogStore } from \"./dialog.store\";\n\nexport const dialogStoreContext = createContext<DialogStore | undefined>(undefined);\nexport const useDialogStore = () => {\n const context = useContext(dialogStoreContext);\n if (!context) {\n throw new Error(\"Dialog store context not available. Are you using the <Dialogs /> component?\");\n }\n return context;\n};\n\nexport const DialogStoreProvider = dialogStoreContext.Provider;\n\nexport const dialogContext = createContext<DialogModel | undefined>(undefined);\nexport const useDialogContext = () => {\n const context = useContext(dialogContext);\n if (!context) {\n throw new Error(\"Dialog context not available. Are you using <DialogProvider /> ?\");\n }\n return context;\n};\n\nexport const useDialogContextIfAvailable = () => useContext(dialogContext);\n\nexport const DialogProvider = dialogContext.Provider;\n","import { makeAutoObservable } from \"mobx\";\nimport type { DialogStore } from \"./dialog.store\";\nimport type { AnyComponent, DialogModelConfig, DialogState } from \"./dialog.types\";\n\nexport class DialogModel {\n readonly id: string;\n readonly component: AnyComponent;\n readonly props?: React.ComponentProps<AnyComponent>;\n\n openedAt = 0;\n previouslyFocusedElement: HTMLElement | null = null;\n\n state!: DialogState;\n\n constructor(\n readonly store: DialogStore,\n readonly config: DialogModelConfig,\n ) {\n this.id = config.id ?? btoa(Math.random().toString()).substring(3, 12);\n this.component = config.component;\n this.props = config.props;\n\n this.setState(config.initialState ?? \"closed\");\n\n makeAutoObservable(this, {\n id: false,\n component: false,\n props: false,\n openedAt: false,\n previouslyFocusedElement: false,\n });\n }\n\n close(closeImmediately?: boolean): void {\n this.setState(closeImmediately ? \"closed\" : \"closing\");\n }\n\n open(openImmediately?: boolean): void {\n this.setState(openImmediately ? \"opened\" : \"opening\");\n }\n\n setState(state: DialogState): void {\n if (this.state === state) return;\n\n this.state = state;\n if (state === \"opened\" || state === \"opening\") {\n this.openedAt = Date.now();\n if (state === \"opening\") {\n this.previouslyFocusedElement =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n }\n } else if (state === \"closed\") {\n this.previouslyFocusedElement?.focus();\n if (this.config.removeOnClosed) {\n this.store.dialogs.delete(this.id);\n }\n this.previouslyFocusedElement = null;\n }\n }\n}\n","import { makeAutoObservable } from \"mobx\";\nimport { DialogModel } from \"./dialog.model\";\nimport type { AnyComponent, DialogComponentAndProps, DialogModelConfig } from \"./dialog.types\";\n\nexport class DialogStore {\n dialogs = new Map<string, DialogModel>();\n\n get openDialogs(): DialogModel[] {\n return [...this.dialogs.values()]\n .filter((d) => d.state !== \"closed\")\n .sort((d1, d2) => d1.openedAt - d2.openedAt);\n }\n\n get activeDialog(): DialogModel | undefined {\n return this.openDialogs.at(-1);\n }\n\n constructor() {\n makeAutoObservable(this);\n }\n\n add(config: DialogModelConfig): DialogModel {\n const dialogModel = new DialogModel(this, config);\n this.dialogs.set(dialogModel.id, dialogModel);\n return dialogModel;\n }\n\n open<C extends AnyComponent>(...args: DialogComponentAndProps<C>): DialogModel {\n const [component, props] = args;\n return this.add({\n component,\n props,\n removeOnClosed: true,\n initialState: \"opening\",\n });\n }\n\n close(closeImmediately?: boolean): void {\n this.activeDialog?.close(closeImmediately);\n }\n}\n","import { useRef } from \"react\";\nimport { DialogStore } from \"./dialog.store\";\n\nexport const useDialogs = (store?: DialogStore): DialogStore => {\n const storeRef = useRef<DialogStore | undefined>(store);\n if (!storeRef.current) {\n storeRef.current = new DialogStore();\n }\n return storeRef.current;\n};\n","import { observer } from \"mobx-react-lite\";\nimport { DialogProvider, DialogStoreProvider } from \"../dialog.context\";\nimport type { DialogModel } from \"../dialog.model\";\nimport type { DialogStore } from \"../dialog.store\";\nimport { useDialogs } from \"../use-dialogs\";\n\nexport interface MobxDialogsProps {\n store?: DialogStore;\n}\n\nexport const MobxDialogs = observer(({ store }: MobxDialogsProps) => {\n const dialogStore = useDialogs(store);\n\n return (\n <DialogStoreProvider value={dialogStore}>\n {dialogStore.openDialogs.map((dialog) => {\n return <MobxDialog key={dialog.id} dialog={dialog} />;\n })}\n </DialogStoreProvider>\n );\n});\n\nexport interface MobxDialogProps {\n dialog: DialogModel;\n}\n\nexport const MobxDialog = observer(({ dialog }: MobxDialogProps) => {\n const { component: DialogComponent, props: dialogProps } = dialog;\n return (\n <DialogProvider value={dialog}>\n <DialogComponent {...dialogProps} />\n </DialogProvider>\n );\n});\n"],"mappings":"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"}
|
package/dist/form.mjs
CHANGED
|
@@ -1,2 +1,173 @@
|
|
|
1
|
-
import{createContext
|
|
1
|
+
import { createContext, forwardRef, useContext, useRef } from "react";
|
|
2
|
+
import { makeAutoObservable, toJS } from "mobx";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import Format from "typebox/format";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import Schema, { Validator } from "typebox/schema";
|
|
7
|
+
import Value from "typebox/value";
|
|
8
|
+
|
|
9
|
+
//#region src/form/form.context.ts
|
|
10
|
+
const formContext = createContext(void 0);
|
|
11
|
+
const useFormContext = () => {
|
|
12
|
+
const context = useContext(formContext);
|
|
13
|
+
if (!context) throw new Error("Form context not available. Make sure you are within the <Form> component or providing the form context manually.");
|
|
14
|
+
return context;
|
|
15
|
+
};
|
|
16
|
+
const FormProvider = formContext.Provider;
|
|
17
|
+
const useFormContextIfAvailable = () => useContext(formContext);
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/form/components/form.tsx
|
|
21
|
+
const MobxForm = forwardRef(function MobxForm({ form, children, ...formProps }, ref) {
|
|
22
|
+
return /* @__PURE__ */ jsx(FormProvider, {
|
|
23
|
+
value: form,
|
|
24
|
+
children: /* @__PURE__ */ jsx("form", {
|
|
25
|
+
noValidate: true,
|
|
26
|
+
...formProps,
|
|
27
|
+
ref,
|
|
28
|
+
...form.props(),
|
|
29
|
+
children
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/form/form-field.model.ts
|
|
36
|
+
var FormFieldModel = class {
|
|
37
|
+
name;
|
|
38
|
+
schema;
|
|
39
|
+
config;
|
|
40
|
+
validator;
|
|
41
|
+
value;
|
|
42
|
+
touched = false;
|
|
43
|
+
get valid() {
|
|
44
|
+
if (this.value === void 0 && Type.IsOptional(this.schema)) return true;
|
|
45
|
+
return this.validator.Check(this.value);
|
|
46
|
+
}
|
|
47
|
+
get errorMessage() {
|
|
48
|
+
if (this.valid || !this.touched) return "";
|
|
49
|
+
const [_, errors] = this.validator.Errors(this.value);
|
|
50
|
+
return errors.at(0)?.message ?? "";
|
|
51
|
+
}
|
|
52
|
+
constructor(config) {
|
|
53
|
+
makeAutoObservable(this, {
|
|
54
|
+
name: false,
|
|
55
|
+
schema: false,
|
|
56
|
+
config: false,
|
|
57
|
+
validator: false
|
|
58
|
+
});
|
|
59
|
+
this.config = config;
|
|
60
|
+
this.name = config.name;
|
|
61
|
+
this.schema = config.schema;
|
|
62
|
+
this.validator = Schema.Compile(this.schema);
|
|
63
|
+
this.setValue(config.initialValue);
|
|
64
|
+
}
|
|
65
|
+
setValue(value) {
|
|
66
|
+
this.value = Value.Convert(this.schema, value);
|
|
67
|
+
}
|
|
68
|
+
setTouched(touched) {
|
|
69
|
+
this.touched = touched;
|
|
70
|
+
}
|
|
71
|
+
reset() {
|
|
72
|
+
this.setTouched(false);
|
|
73
|
+
this.setValue(this.config.initialValue);
|
|
74
|
+
}
|
|
75
|
+
props() {
|
|
76
|
+
return {
|
|
77
|
+
name: this.name,
|
|
78
|
+
onChange: (v) => this.setValue(v),
|
|
79
|
+
value: this.value,
|
|
80
|
+
onBlur: () => {
|
|
81
|
+
this.setTouched(true);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
toJSON() {
|
|
86
|
+
return toJS(this.value);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/form/form.model.ts
|
|
92
|
+
Format.Set("password", () => true);
|
|
93
|
+
Format.Set("phone", () => true);
|
|
94
|
+
var FormModel = class {
|
|
95
|
+
fields;
|
|
96
|
+
config;
|
|
97
|
+
schema;
|
|
98
|
+
submitting = false;
|
|
99
|
+
submitted = false;
|
|
100
|
+
submitError;
|
|
101
|
+
constructor(schema, config) {
|
|
102
|
+
this.schema = schema;
|
|
103
|
+
this.config = config;
|
|
104
|
+
makeAutoObservable(this, {
|
|
105
|
+
fields: false,
|
|
106
|
+
schema: false,
|
|
107
|
+
config: false
|
|
108
|
+
});
|
|
109
|
+
this.fields = Object.entries(schema.properties).reduce((fields, [fieldName, fieldSchema]) => {
|
|
110
|
+
fields[fieldName] = new FormFieldModel({
|
|
111
|
+
name: fieldName,
|
|
112
|
+
schema: fieldSchema,
|
|
113
|
+
initialValue: config?.initialValues?.[fieldName]
|
|
114
|
+
});
|
|
115
|
+
return fields;
|
|
116
|
+
}, {});
|
|
117
|
+
}
|
|
118
|
+
get valid() {
|
|
119
|
+
return Object.values(this.fields).every((field) => field.valid);
|
|
120
|
+
}
|
|
121
|
+
props() {
|
|
122
|
+
return { onSubmit: (e) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
this.setSubmitError(void 0);
|
|
125
|
+
if (!this.validate()) return;
|
|
126
|
+
this.setSubmitting(true);
|
|
127
|
+
this.config.handleSubmit(this.toJSON()).then((resp) => {
|
|
128
|
+
this.setSubmitted(true);
|
|
129
|
+
return resp;
|
|
130
|
+
}).catch((e) => {
|
|
131
|
+
this.setSubmitError(e);
|
|
132
|
+
}).finally(() => {
|
|
133
|
+
this.setSubmitting(false);
|
|
134
|
+
});
|
|
135
|
+
} };
|
|
136
|
+
}
|
|
137
|
+
setSubmitError(error) {
|
|
138
|
+
this.submitError = error;
|
|
139
|
+
}
|
|
140
|
+
setSubmitting(submitting) {
|
|
141
|
+
this.submitting = submitting;
|
|
142
|
+
}
|
|
143
|
+
setSubmitted(submitted) {
|
|
144
|
+
this.submitted = submitted;
|
|
145
|
+
}
|
|
146
|
+
reset() {
|
|
147
|
+
this.setSubmitError(void 0);
|
|
148
|
+
for (const field of Object.values(this.fields)) field.reset();
|
|
149
|
+
}
|
|
150
|
+
validate() {
|
|
151
|
+
for (const field of Object.values(this.fields)) field.setTouched(true);
|
|
152
|
+
return this.valid;
|
|
153
|
+
}
|
|
154
|
+
toJSON() {
|
|
155
|
+
return Object.values(this.fields).reduce((fields, field) => {
|
|
156
|
+
fields[field.name] = field.value;
|
|
157
|
+
return fields;
|
|
158
|
+
}, {});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/form/use-form.ts
|
|
164
|
+
const useForm = (schema, config) => {
|
|
165
|
+
const formRef = useRef(void 0);
|
|
166
|
+
if (formRef.current) Object.assign(formRef.current.config, config);
|
|
167
|
+
else formRef.current = new FormModel(schema, config);
|
|
168
|
+
return formRef.current;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
export { FormFieldModel, FormModel, FormProvider, MobxForm, formContext, useForm, useFormContext, useFormContextIfAvailable };
|
|
2
173
|
//# 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 Value from \"typebox/value\";\nimport { type Static, type TSchema, Type } 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 TSchema = TSchema> {\n readonly name: string;\n readonly schema: T;\n readonly config: FormFieldConfig<T>;\n readonly validator: Validator<T>;\n\n value: Static<T> | undefined;\n touched = false;\n\n get valid(): boolean {\n if (this.value === undefined && Type.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 return errors.at(0)?.message ?? \"\";\n }\n\n constructor(config: FormFieldConfig<T>) {\n makeAutoObservable(this, {\n name: false,\n schema: false,\n config: false,\n validator: false,\n });\n\n this.config = config;\n this.name = config.name;\n this.schema = config.schema;\n this.validator = Schema.Compile(this.schema);\n this.setValue(config.initialValue);\n }\n\n setValue(value?: 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 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?: Static<T>) => this.setValue(v),\n value: this.value,\n onBlur: () => {\n this.setTouched(true);\n },\n };\n }\n\n toJSON(): Static<T> | undefined {\n return toJS(this.value);\n }\n}\n","import 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/*\nSetErrorFunction((param) => {\n const { schema, value } = param;\n\n const message =\n typeof schema.errorMessage === \"function\" ? schema.errorMessage(param) : schema.errorMessage;\n\n if (message !== undefined) return message;\n\n if ((value === undefined || value === \"\") && !TypeGuard.IsOptional(schema)) {\n return \"This field is required.\";\n }\n\n if (Type.IsString(schema)) {\n if (Schema.IsFormat(schema) && schema.format === \"email\")\n return \"Please enter a valid email address.\";\n }\n\n return DefaultErrorFunction(param);\n});\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 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/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 Value from \"typebox/value\";\nimport { type Static, type TSchema, Type } 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 TSchema = TSchema> {\n readonly name: string;\n readonly schema: T;\n readonly config: FormFieldConfig<T>;\n readonly validator: Validator<T>;\n\n value: Static<T> | undefined;\n touched = false;\n\n get valid(): boolean {\n if (this.value === undefined && Type.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 return errors.at(0)?.message ?? \"\";\n }\n\n constructor(config: FormFieldConfig<T>) {\n makeAutoObservable(this, {\n name: false,\n schema: false,\n config: false,\n validator: false,\n });\n\n this.config = config;\n this.name = config.name;\n this.schema = config.schema;\n this.validator = Schema.Compile(this.schema);\n this.setValue(config.initialValue);\n }\n\n setValue(value?: 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 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?: Static<T>) => this.setValue(v),\n value: this.value,\n onBlur: () => {\n this.setTouched(true);\n },\n };\n }\n\n toJSON(): Static<T> | undefined {\n return toJS(this.value);\n }\n}\n","import 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/*\nSetErrorFunction((param) => {\n const { schema, value } = param;\n\n const message =\n typeof schema.errorMessage === \"function\" ? schema.errorMessage(param) : schema.errorMessage;\n\n if (message !== undefined) return message;\n\n if ((value === undefined || value === \"\") && !TypeGuard.IsOptional(schema)) {\n return \"This field is required.\";\n }\n\n if (Type.IsString(schema)) {\n if (Schema.IsFormat(schema) && schema.format === \"email\")\n return \"Please enter a valid email address.\";\n }\n\n return DefaultErrorFunction(param);\n});\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 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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"}
|
package/dist/react-util.mjs
CHANGED
|
@@ -1,2 +1,152 @@
|
|
|
1
|
-
import{useCallback
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/react-util/useAsyncFn.ts
|
|
4
|
+
const useAsyncFn = (fn, deps = [], options) => {
|
|
5
|
+
const { debounceType = "leading", debounceMs = 650 } = options ?? {};
|
|
6
|
+
const timeoutRef = useRef(void 0);
|
|
7
|
+
const abortControllerRef = useRef(void 0);
|
|
8
|
+
const [state, setState] = useState(options?.initialValue ? {
|
|
9
|
+
loading: false,
|
|
10
|
+
value: options?.initialValue
|
|
11
|
+
} : { loading: true });
|
|
12
|
+
const run = useCallback((...args) => {
|
|
13
|
+
window.clearTimeout(timeoutRef.current);
|
|
14
|
+
abortControllerRef.current?.abort();
|
|
15
|
+
const abortController = new AbortController();
|
|
16
|
+
abortControllerRef.current = abortController;
|
|
17
|
+
if (!state?.loading) setState((prevState) => ({
|
|
18
|
+
...prevState,
|
|
19
|
+
loading: true
|
|
20
|
+
}));
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const timeoutMs = debounceType === "leading" && !timeoutRef.current ? 0 : debounceMs;
|
|
23
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
24
|
+
fn(abortController.signal, ...args).then((value) => {
|
|
25
|
+
if (!abortController.signal.aborted) setState({
|
|
26
|
+
value,
|
|
27
|
+
loading: false
|
|
28
|
+
});
|
|
29
|
+
resolve(value);
|
|
30
|
+
}, (error) => {
|
|
31
|
+
if (!abortController.signal.aborted) setState({
|
|
32
|
+
error,
|
|
33
|
+
loading: false
|
|
34
|
+
});
|
|
35
|
+
reject(error);
|
|
36
|
+
}).finally(() => {
|
|
37
|
+
timeoutRef.current = void 0;
|
|
38
|
+
});
|
|
39
|
+
}, timeoutMs);
|
|
40
|
+
});
|
|
41
|
+
}, deps);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
return () => abortControllerRef.current?.abort();
|
|
44
|
+
}, []);
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
run
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/react-util/useAsync.ts
|
|
53
|
+
const useAsync = (fn, deps = [], options) => {
|
|
54
|
+
const firstRun = useRef(true);
|
|
55
|
+
const { runImmediately = true, ...useAsyncFnOptions } = options ?? {};
|
|
56
|
+
const asyncFn = useAsyncFn(fn, deps, useAsyncFnOptions);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const isFirstRun = firstRun.current;
|
|
59
|
+
firstRun.current = false;
|
|
60
|
+
if (!runImmediately && isFirstRun) return;
|
|
61
|
+
asyncFn.run();
|
|
62
|
+
}, [asyncFn.run]);
|
|
63
|
+
return asyncFn;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/react-util/useDebouncedCallback.tsx
|
|
68
|
+
function useDebouncedCallback(callback, deps, options) {
|
|
69
|
+
const ref = useRef({
|
|
70
|
+
...options,
|
|
71
|
+
timeout: null,
|
|
72
|
+
mounted: false,
|
|
73
|
+
leadingTriggered: options?.leading ?? false
|
|
74
|
+
});
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const currentRef = ref.current;
|
|
77
|
+
currentRef.mounted = true;
|
|
78
|
+
currentRef.leadingTriggered = false;
|
|
79
|
+
return () => {
|
|
80
|
+
currentRef.mounted = false;
|
|
81
|
+
window.clearTimeout(currentRef.timeout);
|
|
82
|
+
};
|
|
83
|
+
}, []);
|
|
84
|
+
return useCallback((...params) => {
|
|
85
|
+
const currentRef = ref.current;
|
|
86
|
+
window.clearTimeout(currentRef.timeout);
|
|
87
|
+
if (!currentRef.leadingTriggered && currentRef.leading) {
|
|
88
|
+
currentRef.leadingTriggered = true;
|
|
89
|
+
callback(...params);
|
|
90
|
+
currentRef.timeout = window.setTimeout(() => {
|
|
91
|
+
currentRef.leadingTriggered = true;
|
|
92
|
+
}, currentRef.delayMs ?? 1600);
|
|
93
|
+
} else currentRef.timeout = window.setTimeout(() => {
|
|
94
|
+
if (!currentRef.mounted) return;
|
|
95
|
+
callback(...params);
|
|
96
|
+
currentRef.leadingTriggered = false;
|
|
97
|
+
}, currentRef.delayMs ?? 1600);
|
|
98
|
+
}, deps);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/react-util/useDebouncedEffect.tsx
|
|
103
|
+
function useDebouncedEffect(callback, deps, options) {
|
|
104
|
+
return useEffect(useDebouncedCallback(callback, deps, {
|
|
105
|
+
leading: true,
|
|
106
|
+
...options
|
|
107
|
+
}), deps);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/react-util/useMountEffect.tsx
|
|
112
|
+
const useMountEffect = (effectFn) => useEffect(effectFn, []);
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/react-util/useMountedState.ts
|
|
116
|
+
const useMountedState = () => {
|
|
117
|
+
const mountedRef = useRef(false);
|
|
118
|
+
const get = useCallback(() => mountedRef.current, []);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
mountedRef.current = true;
|
|
121
|
+
return () => {
|
|
122
|
+
mountedRef.current = false;
|
|
123
|
+
};
|
|
124
|
+
}, []);
|
|
125
|
+
return get;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/react-util/useResizeObserver.tsx
|
|
130
|
+
const useResizeObserver = (ref, onResize) => {
|
|
131
|
+
const onResizeRef = useRef(onResize);
|
|
132
|
+
const handleResize = useCallback(() => {
|
|
133
|
+
if (ref.current) onResizeRef.current(ref.current.clientWidth, ref.current.clientHeight);
|
|
134
|
+
}, [ref]);
|
|
135
|
+
useLayoutEffect(() => {
|
|
136
|
+
const el = ref.current;
|
|
137
|
+
if (!el) return;
|
|
138
|
+
handleResize();
|
|
139
|
+
if (typeof ResizeObserver === "function") {
|
|
140
|
+
const resizeObserver = new ResizeObserver(() => handleResize());
|
|
141
|
+
resizeObserver.observe(el);
|
|
142
|
+
return () => resizeObserver.disconnect();
|
|
143
|
+
} else {
|
|
144
|
+
window.addEventListener("resize", handleResize);
|
|
145
|
+
return () => window.window.removeEventListener("resize", handleResize);
|
|
146
|
+
}
|
|
147
|
+
}, [ref, handleResize]);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
export { useAsync, useAsyncFn, useDebouncedCallback, useDebouncedEffect, useMountEffect, useMountedState, useResizeObserver };
|
|
2
152
|
//# sourceMappingURL=react-util.mjs.map
|
package/dist/react-util.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-util.mjs","names":[],"sources":["../src/react-util/useAsyncFn.ts","../src/react-util/useAsync.ts","../src/react-util/useDebouncedCallback.tsx","../src/react-util/useDebouncedEffect.tsx","../src/react-util/useMountEffect.tsx","../src/react-util/useMountedState.ts","../src/react-util/useResizeObserver.tsx"],"sourcesContent":["import { type DependencyList, useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface UseAsyncFnOptions<F extends UseAsyncFn> {\n initialValue?: InferValue<F>;\n debounceMs?: number;\n debounceType?: \"leading\" | \"trailing\";\n}\n\nexport type UseAsyncFn = (signal: AbortSignal, ...args: any[]) => Promise<any>;\nexport type UseAsyncFnRun<Value, Args> = Args extends unknown[]\n ? (...args: Args) => Promise<Value>\n : () => Promise<Value>;\n\nexport interface UseAsyncFnStateBase<Value, Args> {\n run: UseAsyncFnRun<Value, Args>;\n}\n\nexport interface UseAsyncFnStateLoading<Value, Args> extends UseAsyncFnStateBase<Value, Args> {\n loading: true;\n error?: Error | undefined;\n value?: Value;\n}\n\nexport interface UseAsyncFnStateError<Value, Args> extends UseAsyncFnStateBase<Value, Args> {\n loading: false;\n error: Error;\n value?: undefined;\n}\n\nexport interface UseAsyncFnStateResolved<Value, Args> extends UseAsyncFnStateBase<Value, Args> {\n loading: false;\n error?: undefined;\n value: Value;\n}\n\ntype InferValue<F extends UseAsyncFn> = Awaited<ReturnType<F>>;\ntype InferArgs<F extends UseAsyncFn> = Parameters<F> extends [any, ...infer Rest] ? Rest : [];\n\nexport type UseAsyncFnState<Value, Args> =\n | UseAsyncFnStateLoading<Value, Args>\n | UseAsyncFnStateError<Value, Args>\n | UseAsyncFnStateResolved<Value, Args>;\n\nexport const useAsyncFn = <F extends UseAsyncFn, Value = InferValue<F>, Args = InferArgs<F>>(\n fn: F,\n deps: DependencyList = [],\n options?: UseAsyncFnOptions<F>,\n): UseAsyncFnState<Value, Args> => {\n const { debounceType = \"leading\", debounceMs = 650 } = options ?? {};\n\n const timeoutRef = useRef<number | undefined>(undefined);\n const abortControllerRef = useRef<AbortController | undefined>(undefined);\n\n const [state, setState] = useState<Omit<UseAsyncFnState<Value, Args>, \"run\">>(\n options?.initialValue ? { loading: false, value: options?.initialValue } : { loading: true },\n );\n\n const run = useCallback((...args: Parameters<F>) => {\n window.clearTimeout(timeoutRef.current);\n\n abortControllerRef.current?.abort();\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n if (!state?.loading) {\n setState((prevState) => ({ ...prevState, loading: true }));\n }\n\n return new Promise((resolve, reject) => {\n const timeoutMs = debounceType === \"leading\" && !timeoutRef.current ? 0 : debounceMs;\n\n timeoutRef.current = window.setTimeout(() => {\n fn(abortController.signal, ...args)\n .then(\n (value) => {\n if (!abortController.signal.aborted) {\n setState({ value, loading: false });\n }\n resolve(value);\n },\n (error) => {\n if (!abortController.signal.aborted) {\n setState({ error, loading: false });\n }\n reject(error);\n },\n )\n .finally(() => {\n timeoutRef.current = undefined;\n });\n }, timeoutMs);\n });\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: deps are controlled by caller\n }, deps);\n\n useEffect(() => {\n return () => abortControllerRef.current?.abort();\n }, []);\n\n return { ...state, run } as UseAsyncFnState<Value, Args>;\n};\n","import { type DependencyList, useEffect, useRef } from \"react\";\nimport { type UseAsyncFn, type UseAsyncFnOptions, useAsyncFn } from \"./useAsyncFn\";\n\nexport const useAsync = <F extends UseAsyncFn>(\n fn: F,\n deps: DependencyList = [],\n options?: UseAsyncFnOptions<F> & { runImmediately?: boolean },\n) => {\n const firstRun = useRef(true);\n\n const { runImmediately = true, ...useAsyncFnOptions } = options ?? {};\n\n const asyncFn = useAsyncFn(fn, deps, useAsyncFnOptions);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: option changes should not trigger effect\n useEffect(() => {\n const isFirstRun = firstRun.current;\n firstRun.current = false;\n\n if (!runImmediately && isFirstRun) return;\n\n // TODO: how should errors be handled? this isn't great...\n void asyncFn.run();\n }, [asyncFn.run]);\n\n return asyncFn;\n};\n","import { type DependencyList, useCallback, useEffect, useRef } from \"react\";\n\nexport interface UseDebouncedCallbackOptions {\n leading?: boolean;\n delayMs?: number;\n}\n\nexport function useDebouncedCallback<T extends (...args: any) => any>(\n callback: T,\n deps: DependencyList,\n options?: UseDebouncedCallbackOptions,\n) {\n const ref = useRef<\n UseDebouncedCallbackOptions & {\n timeout: number | null;\n mounted: boolean;\n leadingTriggered: boolean;\n }\n >({\n ...options,\n timeout: null,\n mounted: false,\n leadingTriggered: options?.leading ?? false,\n });\n\n useEffect(() => {\n const currentRef = ref.current;\n currentRef.mounted = true;\n currentRef.leadingTriggered = false;\n return () => {\n currentRef.mounted = false;\n window.clearTimeout(currentRef.timeout!);\n };\n }, []);\n\n return useCallback((...params: Parameters<T>) => {\n const currentRef = ref.current;\n window.clearTimeout(currentRef.timeout!);\n if (!currentRef.leadingTriggered && currentRef.leading) {\n currentRef.leadingTriggered = true;\n callback(...params);\n currentRef.timeout = window.setTimeout(() => {\n currentRef.leadingTriggered = true;\n }, currentRef.delayMs ?? 1600);\n } else {\n currentRef.timeout = window.setTimeout(() => {\n if (!currentRef.mounted) return;\n callback(...params);\n currentRef.leadingTriggered = false;\n }, currentRef.delayMs ?? 1600);\n }\n // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n }, deps) as T;\n}\n","import { type DependencyList, type EffectCallback, useEffect } from \"react\";\nimport { type UseDebouncedCallbackOptions, useDebouncedCallback } from \"./useDebouncedCallback\";\n\nexport function useDebouncedEffect(\n callback: EffectCallback,\n deps: DependencyList,\n options?: UseDebouncedCallbackOptions,\n) {\n const debouncedCallback = useDebouncedCallback(callback, deps, { leading: true, ...options });\n // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n return useEffect(debouncedCallback, deps);\n}\n","import { useEffect } from \"react\";\n\nexport const useMountEffect = (effectFn: React.EffectCallback): void => useEffect(effectFn, []);\n","import { useCallback, useEffect, useRef } from \"react\";\n\nexport const useMountedState = (): (() => boolean) => {\n const mountedRef = useRef<boolean>(false);\n const get = useCallback(() => mountedRef.current, []);\n\n useEffect(() => {\n mountedRef.current = true;\n\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n return get;\n};\n","import { useCallback, useLayoutEffect, useRef } from \"react\";\n\nexport const useResizeObserver = (\n ref: React.MutableRefObject<HTMLElement | null>,\n onResize: (width: number, height: number) => void,\n): void => {\n const onResizeRef = useRef(onResize);\n const handleResize = useCallback(() => {\n if (ref.current) {\n onResizeRef.current(ref.current.clientWidth, ref.current.clientHeight);\n }\n }, [ref]);\n\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) {\n return;\n }\n\n handleResize();\n\n if (typeof ResizeObserver === \"function\") {\n const resizeObserver = new ResizeObserver(() => handleResize());\n\n resizeObserver.observe(el);\n\n return () => resizeObserver.disconnect();\n } else {\n window.addEventListener(\"resize\", handleResize);\n\n return () => window.window.removeEventListener(\"resize\", handleResize);\n }\n }, [ref, handleResize]);\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"react-util.mjs","names":[],"sources":["../src/react-util/useAsyncFn.ts","../src/react-util/useAsync.ts","../src/react-util/useDebouncedCallback.tsx","../src/react-util/useDebouncedEffect.tsx","../src/react-util/useMountEffect.tsx","../src/react-util/useMountedState.ts","../src/react-util/useResizeObserver.tsx"],"sourcesContent":["import { type DependencyList, useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface UseAsyncFnOptions<F extends UseAsyncFn> {\n initialValue?: InferValue<F>;\n debounceMs?: number;\n debounceType?: \"leading\" | \"trailing\";\n}\n\nexport type UseAsyncFn = (signal: AbortSignal, ...args: any[]) => Promise<any>;\nexport type UseAsyncFnRun<Value, Args> = Args extends unknown[]\n ? (...args: Args) => Promise<Value>\n : () => Promise<Value>;\n\nexport interface UseAsyncFnStateBase<Value, Args> {\n run: UseAsyncFnRun<Value, Args>;\n}\n\nexport interface UseAsyncFnStateLoading<Value, Args> extends UseAsyncFnStateBase<Value, Args> {\n loading: true;\n error?: Error | undefined;\n value?: Value;\n}\n\nexport interface UseAsyncFnStateError<Value, Args> extends UseAsyncFnStateBase<Value, Args> {\n loading: false;\n error: Error;\n value?: undefined;\n}\n\nexport interface UseAsyncFnStateResolved<Value, Args> extends UseAsyncFnStateBase<Value, Args> {\n loading: false;\n error?: undefined;\n value: Value;\n}\n\ntype InferValue<F extends UseAsyncFn> = Awaited<ReturnType<F>>;\ntype InferArgs<F extends UseAsyncFn> = Parameters<F> extends [any, ...infer Rest] ? Rest : [];\n\nexport type UseAsyncFnState<Value, Args> =\n | UseAsyncFnStateLoading<Value, Args>\n | UseAsyncFnStateError<Value, Args>\n | UseAsyncFnStateResolved<Value, Args>;\n\nexport const useAsyncFn = <F extends UseAsyncFn, Value = InferValue<F>, Args = InferArgs<F>>(\n fn: F,\n deps: DependencyList = [],\n options?: UseAsyncFnOptions<F>,\n): UseAsyncFnState<Value, Args> => {\n const { debounceType = \"leading\", debounceMs = 650 } = options ?? {};\n\n const timeoutRef = useRef<number | undefined>(undefined);\n const abortControllerRef = useRef<AbortController | undefined>(undefined);\n\n const [state, setState] = useState<Omit<UseAsyncFnState<Value, Args>, \"run\">>(\n options?.initialValue ? { loading: false, value: options?.initialValue } : { loading: true },\n );\n\n const run = useCallback((...args: Parameters<F>) => {\n window.clearTimeout(timeoutRef.current);\n\n abortControllerRef.current?.abort();\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n if (!state?.loading) {\n setState((prevState) => ({ ...prevState, loading: true }));\n }\n\n return new Promise((resolve, reject) => {\n const timeoutMs = debounceType === \"leading\" && !timeoutRef.current ? 0 : debounceMs;\n\n timeoutRef.current = window.setTimeout(() => {\n fn(abortController.signal, ...args)\n .then(\n (value) => {\n if (!abortController.signal.aborted) {\n setState({ value, loading: false });\n }\n resolve(value);\n },\n (error) => {\n if (!abortController.signal.aborted) {\n setState({ error, loading: false });\n }\n reject(error);\n },\n )\n .finally(() => {\n timeoutRef.current = undefined;\n });\n }, timeoutMs);\n });\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: deps are controlled by caller\n }, deps);\n\n useEffect(() => {\n return () => abortControllerRef.current?.abort();\n }, []);\n\n return { ...state, run } as UseAsyncFnState<Value, Args>;\n};\n","import { type DependencyList, useEffect, useRef } from \"react\";\nimport { type UseAsyncFn, type UseAsyncFnOptions, useAsyncFn } from \"./useAsyncFn\";\n\nexport const useAsync = <F extends UseAsyncFn>(\n fn: F,\n deps: DependencyList = [],\n options?: UseAsyncFnOptions<F> & { runImmediately?: boolean },\n) => {\n const firstRun = useRef(true);\n\n const { runImmediately = true, ...useAsyncFnOptions } = options ?? {};\n\n const asyncFn = useAsyncFn(fn, deps, useAsyncFnOptions);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: option changes should not trigger effect\n useEffect(() => {\n const isFirstRun = firstRun.current;\n firstRun.current = false;\n\n if (!runImmediately && isFirstRun) return;\n\n // TODO: how should errors be handled? this isn't great...\n void asyncFn.run();\n }, [asyncFn.run]);\n\n return asyncFn;\n};\n","import { type DependencyList, useCallback, useEffect, useRef } from \"react\";\n\nexport interface UseDebouncedCallbackOptions {\n leading?: boolean;\n delayMs?: number;\n}\n\nexport function useDebouncedCallback<T extends (...args: any) => any>(\n callback: T,\n deps: DependencyList,\n options?: UseDebouncedCallbackOptions,\n) {\n const ref = useRef<\n UseDebouncedCallbackOptions & {\n timeout: number | null;\n mounted: boolean;\n leadingTriggered: boolean;\n }\n >({\n ...options,\n timeout: null,\n mounted: false,\n leadingTriggered: options?.leading ?? false,\n });\n\n useEffect(() => {\n const currentRef = ref.current;\n currentRef.mounted = true;\n currentRef.leadingTriggered = false;\n return () => {\n currentRef.mounted = false;\n window.clearTimeout(currentRef.timeout!);\n };\n }, []);\n\n return useCallback((...params: Parameters<T>) => {\n const currentRef = ref.current;\n window.clearTimeout(currentRef.timeout!);\n if (!currentRef.leadingTriggered && currentRef.leading) {\n currentRef.leadingTriggered = true;\n callback(...params);\n currentRef.timeout = window.setTimeout(() => {\n currentRef.leadingTriggered = true;\n }, currentRef.delayMs ?? 1600);\n } else {\n currentRef.timeout = window.setTimeout(() => {\n if (!currentRef.mounted) return;\n callback(...params);\n currentRef.leadingTriggered = false;\n }, currentRef.delayMs ?? 1600);\n }\n // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n }, deps) as T;\n}\n","import { type DependencyList, type EffectCallback, useEffect } from \"react\";\nimport { type UseDebouncedCallbackOptions, useDebouncedCallback } from \"./useDebouncedCallback\";\n\nexport function useDebouncedEffect(\n callback: EffectCallback,\n deps: DependencyList,\n options?: UseDebouncedCallbackOptions,\n) {\n const debouncedCallback = useDebouncedCallback(callback, deps, { leading: true, ...options });\n // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n return useEffect(debouncedCallback, deps);\n}\n","import { useEffect } from \"react\";\n\nexport const useMountEffect = (effectFn: React.EffectCallback): void => useEffect(effectFn, []);\n","import { useCallback, useEffect, useRef } from \"react\";\n\nexport const useMountedState = (): (() => boolean) => {\n const mountedRef = useRef<boolean>(false);\n const get = useCallback(() => mountedRef.current, []);\n\n useEffect(() => {\n mountedRef.current = true;\n\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n return get;\n};\n","import { useCallback, useLayoutEffect, useRef } from \"react\";\n\nexport const useResizeObserver = (\n ref: React.MutableRefObject<HTMLElement | null>,\n onResize: (width: number, height: number) => void,\n): void => {\n const onResizeRef = useRef(onResize);\n const handleResize = useCallback(() => {\n if (ref.current) {\n onResizeRef.current(ref.current.clientWidth, ref.current.clientHeight);\n }\n }, [ref]);\n\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) {\n return;\n }\n\n handleResize();\n\n if (typeof ResizeObserver === \"function\") {\n const resizeObserver = new ResizeObserver(() => handleResize());\n\n resizeObserver.observe(el);\n\n return () => resizeObserver.disconnect();\n } else {\n window.addEventListener(\"resize\", handleResize);\n\n return () => window.window.removeEventListener(\"resize\", handleResize);\n }\n }, [ref, handleResize]);\n};\n"],"mappings":"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"}
|
package/dist/router.d.mts
CHANGED
|
@@ -11,10 +11,10 @@ interface OutletConfig {
|
|
|
11
11
|
type RouteSegmentState = "preloading" | "loading" | "error" | "ready";
|
|
12
12
|
declare class Outlet {
|
|
13
13
|
readonly config: OutletConfig;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
state: RouteSegmentState;
|
|
15
|
+
promise: Promise<unknown> | undefined;
|
|
16
|
+
data: unknown;
|
|
17
|
+
component: Component | undefined;
|
|
18
18
|
get Component(): Component | undefined;
|
|
19
19
|
constructor(config: OutletConfig);
|
|
20
20
|
load(route: Route): Promise<void>;
|