@ariakit/components 0.1.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/CHANGELOG.md +19 -0
- package/dist/checkbox/checkbox-store.d.ts +47 -0
- package/dist/checkbox/checkbox-store.d.ts.map +1 -0
- package/dist/checkbox/checkbox-store.js +16 -0
- package/dist/checkbox/checkbox-store.js.map +1 -0
- package/dist/collection/collection-store.d.ts +2 -0
- package/dist/collection/collection-store.js +132 -0
- package/dist/collection/collection-store.js.map +1 -0
- package/dist/collection-store-yNe83BiS.d.ts +81 -0
- package/dist/collection-store-yNe83BiS.d.ts.map +1 -0
- package/dist/combobox/combobox-store.d.ts +150 -0
- package/dist/combobox/combobox-store.d.ts.map +1 -0
- package/dist/combobox/combobox-store.js +83 -0
- package/dist/combobox/combobox-store.js.map +1 -0
- package/dist/composite/composite-overflow-store.d.ts +16 -0
- package/dist/composite/composite-overflow-store.d.ts.map +1 -0
- package/dist/composite/composite-overflow-store.js +12 -0
- package/dist/composite/composite-overflow-store.js.map +1 -0
- package/dist/composite/composite-store.d.ts +2 -0
- package/dist/composite/composite-store.js +167 -0
- package/dist/composite/composite-store.js.map +1 -0
- package/dist/composite-store-B-iDEtZZ.d.ts +331 -0
- package/dist/composite-store-B-iDEtZZ.d.ts.map +1 -0
- package/dist/dialog/dialog-store.d.ts +2 -0
- package/dist/dialog/dialog-store.js +12 -0
- package/dist/dialog/dialog-store.js.map +1 -0
- package/dist/dialog-store-BOLvw2IX.d.ts +16 -0
- package/dist/dialog-store-BOLvw2IX.d.ts.map +1 -0
- package/dist/disclosure/disclosure-store.d.ts +2 -0
- package/dist/disclosure/disclosure-store.js +47 -0
- package/dist/disclosure/disclosure-store.js.map +1 -0
- package/dist/disclosure-store-xKlQffR0.d.ts +142 -0
- package/dist/disclosure-store-xKlQffR0.d.ts.map +1 -0
- package/dist/form/form-store.d.ts +247 -0
- package/dist/form/form-store.d.ts.map +1 -0
- package/dist/form/form-store.js +211 -0
- package/dist/form/form-store.js.map +1 -0
- package/dist/form/types.d.ts +37 -0
- package/dist/form/types.d.ts.map +1 -0
- package/dist/form/types.js +0 -0
- package/dist/hovercard/hovercard-store.d.ts +65 -0
- package/dist/hovercard/hovercard-store.d.ts.map +1 -0
- package/dist/hovercard/hovercard-store.js +31 -0
- package/dist/hovercard/hovercard-store.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/menu/menu-bar-store.d.ts +16 -0
- package/dist/menu/menu-bar-store.d.ts.map +1 -0
- package/dist/menu/menu-bar-store.js +12 -0
- package/dist/menu/menu-bar-store.js.map +1 -0
- package/dist/menu/menu-store.d.ts +100 -0
- package/dist/menu/menu-store.d.ts.map +1 -0
- package/dist/menu/menu-store.js +74 -0
- package/dist/menu/menu-store.js.map +1 -0
- package/dist/menubar/menubar-store.d.ts +2 -0
- package/dist/menubar/menubar-store.js +24 -0
- package/dist/menubar/menubar-store.js.map +1 -0
- package/dist/menubar-store-CD3YDYfW.d.ts +16 -0
- package/dist/menubar-store-CD3YDYfW.d.ts.map +1 -0
- package/dist/popover/popover-store.d.ts +2 -0
- package/dist/popover/popover-store.js +44 -0
- package/dist/popover/popover-store.js.map +1 -0
- package/dist/popover-store-DoCiTmUQ.d.ts +106 -0
- package/dist/popover-store-DoCiTmUQ.d.ts.map +1 -0
- package/dist/radio/radio-store.d.ts +42 -0
- package/dist/radio/radio-store.d.ts.map +1 -0
- package/dist/radio/radio-store.js +27 -0
- package/dist/radio/radio-store.js.map +1 -0
- package/dist/select/select-store.d.ts +116 -0
- package/dist/select/select-store.d.ts.map +1 -0
- package/dist/select/select-store.js +93 -0
- package/dist/select/select-store.js.map +1 -0
- package/dist/tab/tab-store.d.ts +127 -0
- package/dist/tab/tab-store.d.ts.map +1 -0
- package/dist/tab/tab-store.js +107 -0
- package/dist/tab/tab-store.js.map +1 -0
- package/dist/tag/tag-store.d.ts +2 -0
- package/dist/tag/tag-store.js +60 -0
- package/dist/tag/tag-store.js.map +1 -0
- package/dist/tag-store-D47X5_zA.d.ts +83 -0
- package/dist/tag-store-D47X5_zA.d.ts.map +1 -0
- package/dist/toolbar/toolbar-store.d.ts +21 -0
- package/dist/toolbar/toolbar-store.d.ts.map +1 -0
- package/dist/toolbar/toolbar-store.js +18 -0
- package/dist/toolbar/toolbar-store.js.map +1 -0
- package/dist/tooltip/tooltip-store.d.ts +35 -0
- package/dist/tooltip/tooltip-store.d.ts.map +1 -0
- package/dist/tooltip/tooltip-store.js +29 -0
- package/dist/tooltip/tooltip-store.js.map +1 -0
- package/license +21 -0
- package/package.json +121 -0
- package/readme.md +19 -0
- package/src/checkbox/checkbox-store.ts +93 -0
- package/src/collection/collection-store.ts +301 -0
- package/src/combobox/combobox-store.ts +382 -0
- package/src/composite/composite-overflow-store.ts +30 -0
- package/src/composite/composite-store.ts +711 -0
- package/src/dialog/dialog-store.ts +26 -0
- package/src/disclosure/disclosure-store.ts +226 -0
- package/src/form/form-store.ts +608 -0
- package/src/form/types.ts +44 -0
- package/src/hovercard/hovercard-store.ts +112 -0
- package/src/index.ts +1 -0
- package/src/menu/menu-bar-store.ts +28 -0
- package/src/menu/menu-store.ts +263 -0
- package/src/menubar/menubar-store.ts +51 -0
- package/src/popover/popover-store.ts +170 -0
- package/src/radio/radio-store.ts +80 -0
- package/src/select/select-store.ts +323 -0
- package/src/tab/tab-store.ts +330 -0
- package/src/tag/tag-store.ts +170 -0
- package/src/toolbar/toolbar-store.ts +47 -0
- package/src/tooltip/tooltip-store.ts +93 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createStore,
|
|
3
|
+
init,
|
|
4
|
+
setup,
|
|
5
|
+
sync,
|
|
6
|
+
throwOnConflictingProps,
|
|
7
|
+
} from "@ariakit/store";
|
|
8
|
+
import type { Store, StoreOptions, StoreProps } from "@ariakit/store";
|
|
9
|
+
import { applyState, defaultValue, isInteger, isObject } from "@ariakit/utils";
|
|
10
|
+
import type {
|
|
11
|
+
AnyObject,
|
|
12
|
+
PickRequired,
|
|
13
|
+
SetState,
|
|
14
|
+
SetStateAction,
|
|
15
|
+
} from "@ariakit/utils";
|
|
16
|
+
import type {
|
|
17
|
+
CollectionStoreFunctions,
|
|
18
|
+
CollectionStoreItem,
|
|
19
|
+
CollectionStoreOptions,
|
|
20
|
+
CollectionStoreState,
|
|
21
|
+
} from "../collection/collection-store.ts";
|
|
22
|
+
import { createCollectionStore } from "../collection/collection-store.ts";
|
|
23
|
+
import type { DeepMap, DeepPartial, Names, StringLike } from "./types.ts";
|
|
24
|
+
|
|
25
|
+
type ErrorMessage = string | undefined | null;
|
|
26
|
+
|
|
27
|
+
function nextFrame() {
|
|
28
|
+
return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function hasMessages(object: FormStoreValues): boolean {
|
|
32
|
+
return Object.keys(object).some((key) => {
|
|
33
|
+
if (isObject(object[key])) {
|
|
34
|
+
return hasMessages(object[key]);
|
|
35
|
+
}
|
|
36
|
+
return !!object[key];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function get<T>(
|
|
41
|
+
values: FormStoreValues,
|
|
42
|
+
path: StringLike | string[],
|
|
43
|
+
defaultValue?: T,
|
|
44
|
+
): T {
|
|
45
|
+
const [key, ...rest] = Array.isArray(path) ? path : String(path).split(".");
|
|
46
|
+
if (key == null || !values) {
|
|
47
|
+
return defaultValue as T;
|
|
48
|
+
}
|
|
49
|
+
if (!rest.length) {
|
|
50
|
+
return values[key] ?? defaultValue;
|
|
51
|
+
}
|
|
52
|
+
return get(values[key], rest, defaultValue);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Returns the existing nested value if it is an array or object, otherwise
|
|
56
|
+
// creates a new container based on whether the next key is an integer.
|
|
57
|
+
function getOrCreateNested(nestedValues: unknown, nextKey: string) {
|
|
58
|
+
if (Array.isArray(nestedValues) || isObject(nestedValues)) {
|
|
59
|
+
return nestedValues;
|
|
60
|
+
}
|
|
61
|
+
return isInteger(nextKey) ? [] : {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function set<T extends FormStoreValues | unknown[]>(
|
|
65
|
+
values: T,
|
|
66
|
+
path: StringLike | string[],
|
|
67
|
+
value: unknown,
|
|
68
|
+
): T {
|
|
69
|
+
const [k, ...rest] = Array.isArray(path) ? path : String(path).split(".");
|
|
70
|
+
if (k == null) return values;
|
|
71
|
+
const key = k as keyof T;
|
|
72
|
+
const isIntegerKey = isInteger(key);
|
|
73
|
+
const nextValues = isIntegerKey ? values || [] : values || {};
|
|
74
|
+
const nestedValues = nextValues[key];
|
|
75
|
+
const nextKey = rest[0];
|
|
76
|
+
const result =
|
|
77
|
+
rest.length && nextKey != null
|
|
78
|
+
? set(getOrCreateNested(nestedValues, nextKey), rest, value)
|
|
79
|
+
: value;
|
|
80
|
+
if (isIntegerKey) {
|
|
81
|
+
const index = Number(key);
|
|
82
|
+
if (values && Array.isArray(values)) {
|
|
83
|
+
const copy = [...values];
|
|
84
|
+
copy[index] = result;
|
|
85
|
+
return copy as T;
|
|
86
|
+
}
|
|
87
|
+
const nextValues = [] as unknown as T;
|
|
88
|
+
nextValues[index as keyof T] = result as T[keyof T];
|
|
89
|
+
return nextValues;
|
|
90
|
+
}
|
|
91
|
+
return Object.assign({}, values, { [key]: result });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function setAll<T extends FormStoreValues, V>(values: T, value: V) {
|
|
95
|
+
const result = {} as FormStoreValues;
|
|
96
|
+
const keys = Object.keys(values);
|
|
97
|
+
for (const key of keys) {
|
|
98
|
+
const currentValue = values[key];
|
|
99
|
+
if (Array.isArray(currentValue)) {
|
|
100
|
+
result[key] = currentValue.map((v) => {
|
|
101
|
+
if (isObject(v)) {
|
|
102
|
+
return setAll(v, value);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
});
|
|
106
|
+
} else if (isObject(currentValue)) {
|
|
107
|
+
result[key] = setAll(currentValue, value);
|
|
108
|
+
} else {
|
|
109
|
+
result[key] = value;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result as DeepMap<T, V>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getNameHandler(
|
|
116
|
+
cache: FormStoreValues,
|
|
117
|
+
prevKeys: Array<string | symbol> = [],
|
|
118
|
+
) {
|
|
119
|
+
const handler: ProxyHandler<FormStoreValues> = {
|
|
120
|
+
get(target, key) {
|
|
121
|
+
if (["toString", "valueOf", Symbol.toPrimitive].includes(key)) {
|
|
122
|
+
return () => prevKeys.join(".");
|
|
123
|
+
}
|
|
124
|
+
const nextKeys = [...prevKeys, key];
|
|
125
|
+
const nextKey = nextKeys.join(".");
|
|
126
|
+
if (cache[nextKey]) {
|
|
127
|
+
return cache[nextKey];
|
|
128
|
+
}
|
|
129
|
+
const nextProxy = new Proxy(target, getNameHandler(cache, nextKeys));
|
|
130
|
+
cache[nextKey] = nextProxy;
|
|
131
|
+
return nextProxy;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
return handler;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getStoreCallbacks(
|
|
138
|
+
store?: Store & {
|
|
139
|
+
__unstableCallbacks?: Store<{
|
|
140
|
+
validate: FormStoreCallback[];
|
|
141
|
+
submit: FormStoreCallback[];
|
|
142
|
+
}>;
|
|
143
|
+
},
|
|
144
|
+
) {
|
|
145
|
+
return store?.__unstableCallbacks;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createNames() {
|
|
149
|
+
const cache = Object.create(null);
|
|
150
|
+
return new Proxy(Object.create(null), getNameHandler(cache));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Creates a form store.
|
|
155
|
+
*/
|
|
156
|
+
export function createFormStore<T extends FormStoreValues = FormStoreValues>(
|
|
157
|
+
props: PickRequired<
|
|
158
|
+
FormStoreProps<T>,
|
|
159
|
+
| "values"
|
|
160
|
+
| "defaultValues"
|
|
161
|
+
| "errors"
|
|
162
|
+
| "defaultErrors"
|
|
163
|
+
| "touched"
|
|
164
|
+
| "defaultTouched"
|
|
165
|
+
>,
|
|
166
|
+
): FormStore<T>;
|
|
167
|
+
|
|
168
|
+
export function createFormStore(props: FormStoreProps): FormStore;
|
|
169
|
+
|
|
170
|
+
export function createFormStore(props: FormStoreProps = {}): FormStore {
|
|
171
|
+
throwOnConflictingProps(props, props.store);
|
|
172
|
+
|
|
173
|
+
const syncState = props.store?.getState();
|
|
174
|
+
const collection = createCollectionStore(props);
|
|
175
|
+
|
|
176
|
+
const values = defaultValue(
|
|
177
|
+
props.values,
|
|
178
|
+
syncState?.values,
|
|
179
|
+
props.defaultValues,
|
|
180
|
+
{},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const errors = defaultValue(
|
|
184
|
+
props.errors,
|
|
185
|
+
syncState?.errors,
|
|
186
|
+
props.defaultErrors,
|
|
187
|
+
{},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const touched = defaultValue(
|
|
191
|
+
props.touched,
|
|
192
|
+
syncState?.touched,
|
|
193
|
+
props.defaultTouched,
|
|
194
|
+
{},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const initialState: FormStoreState = {
|
|
198
|
+
...collection.getState(),
|
|
199
|
+
values,
|
|
200
|
+
errors,
|
|
201
|
+
touched,
|
|
202
|
+
validating: defaultValue(syncState?.validating, false),
|
|
203
|
+
submitting: defaultValue(syncState?.submitting, false),
|
|
204
|
+
submitSucceed: defaultValue(syncState?.submitSucceed, 0),
|
|
205
|
+
submitFailed: defaultValue(syncState?.submitFailed, 0),
|
|
206
|
+
valid: !hasMessages(errors),
|
|
207
|
+
};
|
|
208
|
+
const form = createStore(initialState, collection, props.store);
|
|
209
|
+
|
|
210
|
+
const syncCallbacks = getStoreCallbacks(props.store);
|
|
211
|
+
const syncCallbacksState = syncCallbacks?.getState();
|
|
212
|
+
|
|
213
|
+
const callbacksInitialState = {
|
|
214
|
+
validate: syncCallbacksState?.validate || [],
|
|
215
|
+
submit: syncCallbacksState?.submit || [],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const callbacks = createStore(callbacksInitialState, syncCallbacks);
|
|
219
|
+
|
|
220
|
+
setup(form, () => init(callbacks));
|
|
221
|
+
|
|
222
|
+
setup(form, () =>
|
|
223
|
+
sync(form, ["validating", "errors"], (state) => {
|
|
224
|
+
if (state.validating) return;
|
|
225
|
+
form.setState("valid", !hasMessages(state.errors));
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const validate = async () => {
|
|
230
|
+
form.setState("validating", true);
|
|
231
|
+
form.setState("errors", {});
|
|
232
|
+
const validateCallbacks = callbacks.getState().validate;
|
|
233
|
+
try {
|
|
234
|
+
// Run all the validation callbacks sequentially so they run in a
|
|
235
|
+
// predictable order. See https://github.com/ariakit/ariakit/issues/2282
|
|
236
|
+
for (const callback of validateCallbacks) {
|
|
237
|
+
await callback(form.getState());
|
|
238
|
+
}
|
|
239
|
+
// Wait for the next frame to allow the errors to be set on the state.
|
|
240
|
+
await nextFrame();
|
|
241
|
+
return !hasMessages(form.getState().errors);
|
|
242
|
+
} finally {
|
|
243
|
+
form.setState("validating", false);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
...collection,
|
|
249
|
+
...form,
|
|
250
|
+
names: createNames(),
|
|
251
|
+
|
|
252
|
+
setValues: (values) => form.setState("values", values),
|
|
253
|
+
getValue: (name) => get(form.getState().values, name),
|
|
254
|
+
setValue: (name, value) =>
|
|
255
|
+
form.setState("values", (values) => {
|
|
256
|
+
const prevValue = get(values, name);
|
|
257
|
+
const nextValue = applyState(value, prevValue);
|
|
258
|
+
if (nextValue === prevValue) return values;
|
|
259
|
+
return set(values, name, nextValue);
|
|
260
|
+
}),
|
|
261
|
+
pushValue: (name, value) =>
|
|
262
|
+
form.setState("values", (values) => {
|
|
263
|
+
const array = get(values, name, [] as unknown[]);
|
|
264
|
+
return set(values, name, [...array, value]);
|
|
265
|
+
}),
|
|
266
|
+
removeValue: (name, index) =>
|
|
267
|
+
form.setState("values", (values) => {
|
|
268
|
+
const array = get(values, name, [] as unknown[]);
|
|
269
|
+
return set(values, name, [
|
|
270
|
+
...array.slice(0, index),
|
|
271
|
+
null,
|
|
272
|
+
...array.slice(index + 1),
|
|
273
|
+
]);
|
|
274
|
+
}),
|
|
275
|
+
|
|
276
|
+
setErrors: (errors) => form.setState("errors", errors),
|
|
277
|
+
getError: (name) => get(form.getState().errors, name),
|
|
278
|
+
setError: (name, error) =>
|
|
279
|
+
form.setState("errors", (errors) => {
|
|
280
|
+
const prevError = get(errors, name);
|
|
281
|
+
const nextError = applyState(error, prevError);
|
|
282
|
+
if (nextError === prevError) return errors;
|
|
283
|
+
return set(errors, name, nextError);
|
|
284
|
+
}),
|
|
285
|
+
|
|
286
|
+
setTouched: (touched) => form.setState("touched", touched),
|
|
287
|
+
getFieldTouched: (name) => !!get(form.getState().touched, name),
|
|
288
|
+
setFieldTouched: (name, value) =>
|
|
289
|
+
form.setState("touched", (touched) => {
|
|
290
|
+
const prevValue = get(touched, name);
|
|
291
|
+
const nextValue = applyState(value, prevValue);
|
|
292
|
+
if (nextValue === prevValue) return touched;
|
|
293
|
+
return set(touched, name, nextValue);
|
|
294
|
+
}),
|
|
295
|
+
|
|
296
|
+
onValidate: (callback) => {
|
|
297
|
+
callbacks.setState("validate", (callbacks) => [...callbacks, callback]);
|
|
298
|
+
return () => {
|
|
299
|
+
callbacks.setState("validate", (callbacks) =>
|
|
300
|
+
callbacks.filter((c) => c !== callback),
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
},
|
|
304
|
+
validate,
|
|
305
|
+
|
|
306
|
+
onSubmit: (callback) => {
|
|
307
|
+
callbacks.setState("submit", (callbacks) => [...callbacks, callback]);
|
|
308
|
+
return () => {
|
|
309
|
+
callbacks.setState("submit", (callbacks) =>
|
|
310
|
+
callbacks.filter((c) => c !== callback),
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
submit: async () => {
|
|
315
|
+
form.setState("submitting", true);
|
|
316
|
+
form.setState("touched", setAll(form.getState().values, true));
|
|
317
|
+
try {
|
|
318
|
+
if (await validate()) {
|
|
319
|
+
const submitCallbacks = callbacks.getState().submit;
|
|
320
|
+
// Run all the submit callbacks sequentially so they run in a
|
|
321
|
+
// predictable order. See
|
|
322
|
+
// https://github.com/ariakit/ariakit/issues/2282
|
|
323
|
+
for (const callback of submitCallbacks) {
|
|
324
|
+
await callback(form.getState());
|
|
325
|
+
}
|
|
326
|
+
// Wait for the next frame to allow the errors to be set on the state.
|
|
327
|
+
await nextFrame();
|
|
328
|
+
if (!hasMessages(form.getState().errors)) {
|
|
329
|
+
form.setState("submitSucceed", (count) => count + 1);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
form.setState("submitFailed", (count) => count + 1);
|
|
334
|
+
return false;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
form.setState("submitFailed", (count) => count + 1);
|
|
337
|
+
throw error;
|
|
338
|
+
} finally {
|
|
339
|
+
form.setState("submitting", false);
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
reset: () => {
|
|
344
|
+
form.setState("values", values);
|
|
345
|
+
form.setState("errors", errors);
|
|
346
|
+
form.setState("touched", touched);
|
|
347
|
+
form.setState("validating", false);
|
|
348
|
+
form.setState("submitting", false);
|
|
349
|
+
form.setState("submitSucceed", 0);
|
|
350
|
+
form.setState("submitFailed", 0);
|
|
351
|
+
form.setState("valid", !hasMessages(errors));
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
// @ts-expect-error Internal
|
|
355
|
+
__unstableCallbacks: callbacks,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export type FormStoreCallback<T extends FormStoreState = FormStoreState> = (
|
|
360
|
+
state: T,
|
|
361
|
+
) => void | Promise<void>;
|
|
362
|
+
|
|
363
|
+
export type FormStoreValues = AnyObject;
|
|
364
|
+
|
|
365
|
+
export interface FormStoreItem extends CollectionStoreItem {
|
|
366
|
+
type: "field" | "label" | "description" | "error" | "button";
|
|
367
|
+
name: string;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export interface FormStoreState<
|
|
371
|
+
T extends FormStoreValues = FormStoreValues,
|
|
372
|
+
> extends CollectionStoreState<FormStoreItem> {
|
|
373
|
+
/**
|
|
374
|
+
* Form values.
|
|
375
|
+
*
|
|
376
|
+
* Live examples:
|
|
377
|
+
* - [FormRadio](https://ariakit.com/examples/form-radio)
|
|
378
|
+
* - [FormSelect](https://ariakit.com/examples/form-select)
|
|
379
|
+
* @default {}
|
|
380
|
+
*/
|
|
381
|
+
values: T;
|
|
382
|
+
/**
|
|
383
|
+
* Form errors.
|
|
384
|
+
*/
|
|
385
|
+
errors: DeepPartial<DeepMap<T, ErrorMessage>>;
|
|
386
|
+
/**
|
|
387
|
+
* The touched state of the form.
|
|
388
|
+
*/
|
|
389
|
+
touched: DeepPartial<DeepMap<T, boolean>>;
|
|
390
|
+
/**
|
|
391
|
+
* Whether the form is valid.
|
|
392
|
+
*/
|
|
393
|
+
valid: boolean;
|
|
394
|
+
/**
|
|
395
|
+
* Whether the form is validating.
|
|
396
|
+
*/
|
|
397
|
+
validating: boolean;
|
|
398
|
+
/**
|
|
399
|
+
* Whether the form is submitting.
|
|
400
|
+
*/
|
|
401
|
+
submitting: boolean;
|
|
402
|
+
/**
|
|
403
|
+
* The number of times
|
|
404
|
+
* [`submit`](https://ariakit.com/reference/use-form-store#submit) has been
|
|
405
|
+
* called with a successful response.
|
|
406
|
+
*/
|
|
407
|
+
submitSucceed: number;
|
|
408
|
+
/**
|
|
409
|
+
* The number of times
|
|
410
|
+
* [`submit`](https://ariakit.com/reference/use-form-store#submit) has been
|
|
411
|
+
* called with an error response.
|
|
412
|
+
*/
|
|
413
|
+
submitFailed: number;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface FormStoreFunctions<
|
|
417
|
+
T extends FormStoreValues = FormStoreValues,
|
|
418
|
+
> extends CollectionStoreFunctions<FormStoreItem> {
|
|
419
|
+
/**
|
|
420
|
+
* An object containing the names of the form fields for type safety.
|
|
421
|
+
*
|
|
422
|
+
* Live examples:
|
|
423
|
+
* - [FormRadio](https://ariakit.com/examples/form-radio)
|
|
424
|
+
* - [FormSelect](https://ariakit.com/examples/form-select)
|
|
425
|
+
* @example
|
|
426
|
+
* store.names.name; // "name"
|
|
427
|
+
* store.names.name.first; // "name.first"
|
|
428
|
+
* store.names.name.last; // "name.last"
|
|
429
|
+
*/
|
|
430
|
+
names: Names<T>;
|
|
431
|
+
/**
|
|
432
|
+
* Sets the [`values`](https://ariakit.com/reference/form-provider#values)
|
|
433
|
+
* state.
|
|
434
|
+
* @example
|
|
435
|
+
* store.setValues({ name: "John" });
|
|
436
|
+
* store.setValues((values) => ({ ...values, name: "John" }));
|
|
437
|
+
*/
|
|
438
|
+
setValues: SetState<FormStoreState<T>["values"]>;
|
|
439
|
+
/**
|
|
440
|
+
* Retrieves a field value.
|
|
441
|
+
*
|
|
442
|
+
* Live examples:
|
|
443
|
+
* - [FormRadio](https://ariakit.com/examples/form-radio)
|
|
444
|
+
* @example
|
|
445
|
+
* const nameValue = store.getValue("name");
|
|
446
|
+
* // Can also use store.names for type-safety.
|
|
447
|
+
* const emailValue = store.getValue(store.names.email);
|
|
448
|
+
*/
|
|
449
|
+
// oxlint-disable-next-line no-unnecessary-type-parameters
|
|
450
|
+
getValue: <T = any>(name: StringLike) => T;
|
|
451
|
+
/**
|
|
452
|
+
* Sets a field value.
|
|
453
|
+
*
|
|
454
|
+
* Live examples:
|
|
455
|
+
* - [FormSelect](https://ariakit.com/examples/form-select)
|
|
456
|
+
* @example
|
|
457
|
+
* store.setValue("name", "John");
|
|
458
|
+
* store.setValue("name", (value) => value + " Doe");
|
|
459
|
+
* // Can also use store.names for type-safety.
|
|
460
|
+
* store.setValue(store.names.name, "John");
|
|
461
|
+
*/
|
|
462
|
+
setValue: <T>(name: StringLike, value: SetStateAction<T>) => void;
|
|
463
|
+
/**
|
|
464
|
+
* Pushes a value to an array field.
|
|
465
|
+
* @example
|
|
466
|
+
* store.pushValue("tags", "new tag");
|
|
467
|
+
* store.pushValue("tags", { id: 1, name: "new tag" });
|
|
468
|
+
* // Can also use store.names for type-safety.
|
|
469
|
+
* store.pushValue(store.names.tags, "new tag");
|
|
470
|
+
*/
|
|
471
|
+
// oxlint-disable-next-line no-unnecessary-type-parameters
|
|
472
|
+
pushValue: <T>(name: StringLike, value: T) => void;
|
|
473
|
+
/**
|
|
474
|
+
* Removes a value from an array field.
|
|
475
|
+
* @example
|
|
476
|
+
* store.removeValue("tags", 0);
|
|
477
|
+
* store.removeValue("tags", 1);
|
|
478
|
+
* // Can also use store.names for type-safety.
|
|
479
|
+
* store.removeValue(store.names.tags, 0);
|
|
480
|
+
*/
|
|
481
|
+
removeValue: (name: StringLike, index: number) => void;
|
|
482
|
+
/**
|
|
483
|
+
* Sets the [`errors`](https://ariakit.com/reference/form-provider#errors)
|
|
484
|
+
* state.
|
|
485
|
+
* @example
|
|
486
|
+
* store.setErrors({ name: "Name is required" });
|
|
487
|
+
* store.setErrors((errors) => ({ ...errors, name: "Name is required" }));
|
|
488
|
+
*/
|
|
489
|
+
setErrors: SetState<FormStoreState<T>["errors"]>;
|
|
490
|
+
/**
|
|
491
|
+
* Retrieves a field error.
|
|
492
|
+
* @example
|
|
493
|
+
* const nameError = store.getError("name");
|
|
494
|
+
* // Can also use store.names for type-safety.
|
|
495
|
+
* const emailError = store.getError(store.names.email);
|
|
496
|
+
*/
|
|
497
|
+
getError: (name: StringLike) => ErrorMessage;
|
|
498
|
+
/**
|
|
499
|
+
* Sets a field error.
|
|
500
|
+
*
|
|
501
|
+
* Live examples:
|
|
502
|
+
* - [FormRadio](https://ariakit.com/examples/form-radio)
|
|
503
|
+
* @example
|
|
504
|
+
* store.setError("name", "Name is required");
|
|
505
|
+
* store.setError("name", (error) => error + "!");
|
|
506
|
+
* // Can also use store.names for type-safety.
|
|
507
|
+
* store.setError(store.names.name, "Name is required");
|
|
508
|
+
*/
|
|
509
|
+
setError: (name: StringLike, error: SetStateAction<ErrorMessage>) => void;
|
|
510
|
+
/**
|
|
511
|
+
* Sets the [`touched`](https://ariakit.com/reference/form-provider#touched)
|
|
512
|
+
* state.
|
|
513
|
+
* @example
|
|
514
|
+
* store.setTouched({ name: true });
|
|
515
|
+
* store.setTouched((touched) => ({ ...touched, name: true }));
|
|
516
|
+
*/
|
|
517
|
+
setTouched: SetState<FormStoreState<T>["touched"]>;
|
|
518
|
+
/**
|
|
519
|
+
* Retrieves a field touched state.
|
|
520
|
+
* @example
|
|
521
|
+
* const nameTouched = store.getFieldTouched("name");
|
|
522
|
+
* // Can also use store.names for type-safety.
|
|
523
|
+
* const emailTouched = store.getFieldTouched(store.names.email);
|
|
524
|
+
*/
|
|
525
|
+
getFieldTouched: (name: StringLike) => boolean;
|
|
526
|
+
/**
|
|
527
|
+
* Sets a field touched state.
|
|
528
|
+
* @example
|
|
529
|
+
* store.setFieldTouched("name", true);
|
|
530
|
+
* store.setFieldTouched("name", (value) => !value);
|
|
531
|
+
* // Can also use store.names for type-safety.
|
|
532
|
+
* store.setFieldTouched(store.names.name, true);
|
|
533
|
+
*/
|
|
534
|
+
setFieldTouched: (name: StringLike, value: SetStateAction<boolean>) => void;
|
|
535
|
+
/**
|
|
536
|
+
* Function that accepts a callback that will be used to validate the form
|
|
537
|
+
* when [`validate`](https://ariakit.com/reference/use-form-store#validate) is
|
|
538
|
+
* called. It returns a cleanup function that will remove the callback.
|
|
539
|
+
* @example
|
|
540
|
+
* const cleanup = store.onValidate(async (state) => {
|
|
541
|
+
* const errors = await api.validate(state.values);
|
|
542
|
+
* if (errors) {
|
|
543
|
+
* store.setErrors(errors);
|
|
544
|
+
* }
|
|
545
|
+
* });
|
|
546
|
+
*/
|
|
547
|
+
onValidate: (callback: FormStoreCallback<FormStoreState<T>>) => () => void;
|
|
548
|
+
/**
|
|
549
|
+
* Function that accepts a callback that will be used to submit the form when
|
|
550
|
+
* [`submit`](https://ariakit.com/reference/use-form-store#submit) is called.
|
|
551
|
+
* It returns a cleanup function that will remove the callback.
|
|
552
|
+
* @param callback The callback function.
|
|
553
|
+
* @example
|
|
554
|
+
* const cleanup = store.onSubmit(async (state) => {
|
|
555
|
+
* try {
|
|
556
|
+
* await api.submit(state.values);
|
|
557
|
+
* } catch (errors) {
|
|
558
|
+
* store.setErrors(errors);
|
|
559
|
+
* }
|
|
560
|
+
* });
|
|
561
|
+
*/
|
|
562
|
+
onSubmit: (callback: FormStoreCallback<FormStoreState<T>>) => () => void;
|
|
563
|
+
/**
|
|
564
|
+
* Validates the form.
|
|
565
|
+
* @example
|
|
566
|
+
* if (await store.validate()) {
|
|
567
|
+
* // Form is valid.
|
|
568
|
+
* }
|
|
569
|
+
*/
|
|
570
|
+
validate: () => Promise<boolean>;
|
|
571
|
+
/**
|
|
572
|
+
* Submits the form. This also triggers validation.
|
|
573
|
+
* @example
|
|
574
|
+
* if (await form.submit()) {
|
|
575
|
+
* // Form is submitted.
|
|
576
|
+
* }
|
|
577
|
+
*/
|
|
578
|
+
submit: () => Promise<boolean>;
|
|
579
|
+
/**
|
|
580
|
+
* Resets the form to its default values.
|
|
581
|
+
*/
|
|
582
|
+
reset: () => void;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export interface FormStoreOptions<T extends FormStoreValues = FormStoreValues>
|
|
586
|
+
extends
|
|
587
|
+
CollectionStoreOptions<FormStoreItem>,
|
|
588
|
+
StoreOptions<FormStoreState<T>, "values" | "errors" | "touched"> {
|
|
589
|
+
/**
|
|
590
|
+
* The default values of the form.
|
|
591
|
+
* @default {}
|
|
592
|
+
*/
|
|
593
|
+
defaultValues?: FormStoreState<T>["values"];
|
|
594
|
+
/**
|
|
595
|
+
* The default errors of the form.
|
|
596
|
+
*/
|
|
597
|
+
defaultErrors?: FormStoreState<T>["errors"];
|
|
598
|
+
/**
|
|
599
|
+
* The default touched state of the form.
|
|
600
|
+
*/
|
|
601
|
+
defaultTouched?: FormStoreState<T>["touched"];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export interface FormStoreProps<T extends FormStoreValues = FormStoreValues>
|
|
605
|
+
extends FormStoreOptions<T>, StoreProps<FormStoreState<T>> {}
|
|
606
|
+
|
|
607
|
+
export interface FormStore<T extends FormStoreValues = FormStoreValues>
|
|
608
|
+
extends FormStoreFunctions<T>, Store<FormStoreState<T>> {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AnyObject } from "@ariakit/utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An object or primitive value that can be converted to a string.
|
|
5
|
+
*/
|
|
6
|
+
export interface StringLike {
|
|
7
|
+
toString: () => string;
|
|
8
|
+
valueOf: () => string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maps through an object `T` or array and defines the leaf values to the given
|
|
13
|
+
* type `V`.
|
|
14
|
+
* @template T Object
|
|
15
|
+
* @template V Value
|
|
16
|
+
*/
|
|
17
|
+
export type DeepMap<T, V> = {
|
|
18
|
+
[K in keyof T]: T[K] extends AnyObject ? DeepMap<T[K], V> : V;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Similar to `Partial<T>`, but recursively maps through the object and makes
|
|
23
|
+
* nested object properties optional.
|
|
24
|
+
* @template T Object
|
|
25
|
+
*/
|
|
26
|
+
export type DeepPartial<T> = {
|
|
27
|
+
[K in keyof T]?: T[K] extends AnyObject ? DeepPartial<T[K]> : T[K];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Maps through the values object `T` and defines all properties into a string
|
|
32
|
+
* like type. That is, a type that is an object that can contain other
|
|
33
|
+
* properties but can also be converted into a string with the path name.
|
|
34
|
+
* @template T Values object
|
|
35
|
+
*/
|
|
36
|
+
export type Names<T> = {
|
|
37
|
+
[K in keyof T]: T[K] extends Array<infer U>
|
|
38
|
+
? U extends AnyObject
|
|
39
|
+
? { [key: number]: Names<U> } & StringLike
|
|
40
|
+
: { [key: number]: U & StringLike } & StringLike
|
|
41
|
+
: T[K] extends AnyObject
|
|
42
|
+
? Names<T[K]>
|
|
43
|
+
: StringLike;
|
|
44
|
+
};
|