remix-validated-form 5.0.2 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/ValidatedForm.tsx +0 -427
- package/src/hooks.ts +0 -160
- package/src/index.ts +0 -12
- package/src/internal/MultiValueMap.ts +0 -44
- package/src/internal/constants.ts +0 -4
- package/src/internal/flatten.ts +0 -12
- package/src/internal/formContext.ts +0 -13
- package/src/internal/getInputProps.test.ts +0 -251
- package/src/internal/getInputProps.ts +0 -94
- package/src/internal/hooks.ts +0 -217
- package/src/internal/hydratable.ts +0 -28
- package/src/internal/logic/getCheckboxChecked.ts +0 -10
- package/src/internal/logic/getRadioChecked.ts +0 -18
- package/src/internal/logic/nestedObjectToPathObject.ts +0 -63
- package/src/internal/logic/requestSubmit.test.tsx +0 -24
- package/src/internal/logic/requestSubmit.ts +0 -103
- package/src/internal/state/arrayUtil.ts +0 -451
- package/src/internal/state/controlledFields.ts +0 -86
- package/src/internal/state/createFormStore.ts +0 -591
- package/src/internal/state/fieldArray.tsx +0 -197
- package/src/internal/state/storeHooks.ts +0 -9
- package/src/internal/state/types.ts +0 -1
- package/src/internal/submissionCallbacks.ts +0 -15
- package/src/internal/util.ts +0 -39
- package/src/server.ts +0 -53
- package/src/unreleased/formStateHooks.ts +0 -170
- package/src/userFacingFormContext.ts +0 -147
- package/src/validation/createValidator.ts +0 -53
- package/src/validation/types.ts +0 -72
- package/tsconfig.json +0 -8
package/package.json
CHANGED
package/src/ValidatedForm.tsx
DELETED
@@ -1,427 +0,0 @@
|
|
1
|
-
import {
|
2
|
-
FetcherWithComponents,
|
3
|
-
Form as RemixForm,
|
4
|
-
FormMethod,
|
5
|
-
useSubmit,
|
6
|
-
SubmitOptions,
|
7
|
-
FormEncType,
|
8
|
-
} from "@remix-run/react";
|
9
|
-
import React, {
|
10
|
-
ComponentProps,
|
11
|
-
FormEvent,
|
12
|
-
RefObject,
|
13
|
-
useCallback,
|
14
|
-
useEffect,
|
15
|
-
useMemo,
|
16
|
-
useRef,
|
17
|
-
useState,
|
18
|
-
} from "react";
|
19
|
-
import * as R from "remeda";
|
20
|
-
import { useIsSubmitting, useIsValid } from "./hooks";
|
21
|
-
import { FORM_ID_FIELD } from "./internal/constants";
|
22
|
-
import {
|
23
|
-
InternalFormContext,
|
24
|
-
InternalFormContextValue,
|
25
|
-
} from "./internal/formContext";
|
26
|
-
import {
|
27
|
-
useDefaultValuesFromLoader,
|
28
|
-
useErrorResponseForForm,
|
29
|
-
useHasActiveFormSubmit,
|
30
|
-
useSetFieldErrors,
|
31
|
-
} from "./internal/hooks";
|
32
|
-
import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
|
33
|
-
import {
|
34
|
-
SyncedFormProps,
|
35
|
-
useRootFormStore,
|
36
|
-
} from "./internal/state/createFormStore";
|
37
|
-
import { useFormStore } from "./internal/state/storeHooks";
|
38
|
-
import { useSubmitComplete } from "./internal/submissionCallbacks";
|
39
|
-
import {
|
40
|
-
mergeRefs,
|
41
|
-
useDeepEqualsMemo,
|
42
|
-
useIsomorphicLayoutEffect as useLayoutEffect,
|
43
|
-
} from "./internal/util";
|
44
|
-
import { FieldErrors, Validator } from "./validation/types";
|
45
|
-
|
46
|
-
type SubactionData<
|
47
|
-
DataType,
|
48
|
-
Subaction extends string | undefined
|
49
|
-
> = DataType & { subaction: Subaction };
|
50
|
-
|
51
|
-
// Not all validation libraries support encoding a literal value in the schema type (e.g. yup).
|
52
|
-
// This condition here allows us to provide strictness for users who are using a validation library that does support it,
|
53
|
-
// but also allows us to support users who are using a validation library that doesn't support it.
|
54
|
-
type DataForSubaction<
|
55
|
-
DataType,
|
56
|
-
Subaction extends string | undefined
|
57
|
-
> = Subaction extends string // Not all validation libraries support encoding a literal value in the schema type.
|
58
|
-
? SubactionData<DataType, Subaction> extends undefined
|
59
|
-
? DataType
|
60
|
-
: SubactionData<DataType, Subaction>
|
61
|
-
: DataType;
|
62
|
-
|
63
|
-
export type FormProps<DataType, Subaction extends string | undefined> = {
|
64
|
-
/**
|
65
|
-
* A `Validator` object that describes how to validate the form.
|
66
|
-
*/
|
67
|
-
validator: Validator<DataType>;
|
68
|
-
/**
|
69
|
-
* A submit callback that gets called when the form is submitted
|
70
|
-
* after all validations have been run.
|
71
|
-
*/
|
72
|
-
onSubmit?: (
|
73
|
-
data: DataForSubaction<DataType, Subaction>,
|
74
|
-
event: React.FormEvent<HTMLFormElement>
|
75
|
-
) => void | Promise<void>;
|
76
|
-
/**
|
77
|
-
* Allows you to provide a `fetcher` from Remix's `useFetcher` hook.
|
78
|
-
* The form will use the fetcher for loading states, action data, etc
|
79
|
-
* instead of the default form action.
|
80
|
-
*/
|
81
|
-
fetcher?: FetcherWithComponents<any>;
|
82
|
-
/**
|
83
|
-
* Accepts an object of default values for the form
|
84
|
-
* that will automatically be propagated to the form fields via `useField`.
|
85
|
-
*/
|
86
|
-
defaultValues?: Partial<DataForSubaction<DataType, Subaction>>;
|
87
|
-
/**
|
88
|
-
* A ref to the form element.
|
89
|
-
*/
|
90
|
-
formRef?: React.RefObject<HTMLFormElement>;
|
91
|
-
/**
|
92
|
-
* An optional sub-action to use for the form.
|
93
|
-
* Setting a value here will cause the form to be submitted with an extra `subaction` value.
|
94
|
-
* This can be useful when there are multiple forms on the screen handled by the same action.
|
95
|
-
*/
|
96
|
-
subaction?: Subaction;
|
97
|
-
/**
|
98
|
-
* Reset the form to the default values after the form has been successfully submitted.
|
99
|
-
* This is useful if you want to submit the same form multiple times,
|
100
|
-
* and don't redirect in-between submissions.
|
101
|
-
*/
|
102
|
-
resetAfterSubmit?: boolean;
|
103
|
-
/**
|
104
|
-
* Normally, the first invalid input will be focused when the validation fails on form submit.
|
105
|
-
* Set this to `false` to disable this behavior.
|
106
|
-
*/
|
107
|
-
disableFocusOnError?: boolean;
|
108
|
-
} & Omit<ComponentProps<typeof RemixForm>, "onSubmit">;
|
109
|
-
|
110
|
-
const getDataFromForm = (el: HTMLFormElement) => new FormData(el);
|
111
|
-
|
112
|
-
function nonNull<T>(value: T | null | undefined): value is T {
|
113
|
-
return value !== null;
|
114
|
-
}
|
115
|
-
|
116
|
-
const focusFirstInvalidInput = (
|
117
|
-
fieldErrors: FieldErrors,
|
118
|
-
customFocusHandlers: MultiValueMap<string, () => void>,
|
119
|
-
formElement: HTMLFormElement
|
120
|
-
) => {
|
121
|
-
const namesInOrder = [...formElement.elements]
|
122
|
-
.map((el) => {
|
123
|
-
const input = el instanceof RadioNodeList ? el[0] : el;
|
124
|
-
if (input instanceof HTMLElement && "name" in input)
|
125
|
-
return (input as any).name;
|
126
|
-
return null;
|
127
|
-
})
|
128
|
-
.filter(nonNull)
|
129
|
-
.filter((name) => name in fieldErrors);
|
130
|
-
const uniqueNamesInOrder = R.uniq(namesInOrder);
|
131
|
-
|
132
|
-
for (const fieldName of uniqueNamesInOrder) {
|
133
|
-
if (customFocusHandlers.has(fieldName)) {
|
134
|
-
customFocusHandlers.getAll(fieldName).forEach((handler) => {
|
135
|
-
handler();
|
136
|
-
});
|
137
|
-
break;
|
138
|
-
}
|
139
|
-
|
140
|
-
const elem = formElement.elements.namedItem(fieldName);
|
141
|
-
if (!elem) continue;
|
142
|
-
|
143
|
-
if (elem instanceof RadioNodeList) {
|
144
|
-
const selectedRadio =
|
145
|
-
[...elem]
|
146
|
-
.filter(
|
147
|
-
(item): item is HTMLInputElement => item instanceof HTMLInputElement
|
148
|
-
)
|
149
|
-
.find((item) => item.value === elem.value) ?? elem[0];
|
150
|
-
if (selectedRadio && selectedRadio instanceof HTMLInputElement) {
|
151
|
-
selectedRadio.focus();
|
152
|
-
break;
|
153
|
-
}
|
154
|
-
}
|
155
|
-
|
156
|
-
if (elem instanceof HTMLElement) {
|
157
|
-
if (elem instanceof HTMLInputElement && elem.type === "hidden") {
|
158
|
-
continue;
|
159
|
-
}
|
160
|
-
|
161
|
-
elem.focus();
|
162
|
-
break;
|
163
|
-
}
|
164
|
-
}
|
165
|
-
};
|
166
|
-
|
167
|
-
const useFormId = (providedId?: string): string | symbol => {
|
168
|
-
// We can use a `Symbol` here because we only use it after hydration
|
169
|
-
const [symbolId] = useState(() => Symbol("remix-validated-form-id"));
|
170
|
-
return providedId ?? symbolId;
|
171
|
-
};
|
172
|
-
|
173
|
-
/**
|
174
|
-
* Use a component to access the state so we don't cause
|
175
|
-
* any extra rerenders of the whole form.
|
176
|
-
*/
|
177
|
-
const FormResetter = ({
|
178
|
-
resetAfterSubmit,
|
179
|
-
formRef,
|
180
|
-
}: {
|
181
|
-
resetAfterSubmit: boolean;
|
182
|
-
formRef: RefObject<HTMLFormElement>;
|
183
|
-
}) => {
|
184
|
-
const isSubmitting = useIsSubmitting();
|
185
|
-
const isValid = useIsValid();
|
186
|
-
useSubmitComplete(isSubmitting, () => {
|
187
|
-
if (isValid && resetAfterSubmit) {
|
188
|
-
formRef.current?.reset();
|
189
|
-
}
|
190
|
-
});
|
191
|
-
return null;
|
192
|
-
};
|
193
|
-
|
194
|
-
function formEventProxy<T extends object>(event: T): T {
|
195
|
-
let defaultPrevented = false;
|
196
|
-
return new Proxy(event, {
|
197
|
-
get: (target, prop) => {
|
198
|
-
if (prop === "preventDefault") {
|
199
|
-
return () => {
|
200
|
-
defaultPrevented = true;
|
201
|
-
};
|
202
|
-
}
|
203
|
-
|
204
|
-
if (prop === "defaultPrevented") {
|
205
|
-
return defaultPrevented;
|
206
|
-
}
|
207
|
-
|
208
|
-
return target[prop as keyof T];
|
209
|
-
},
|
210
|
-
}) as T;
|
211
|
-
}
|
212
|
-
|
213
|
-
type HTMLSubmitEvent = React.BaseSyntheticEvent<
|
214
|
-
SubmitEvent,
|
215
|
-
Event,
|
216
|
-
HTMLFormElement
|
217
|
-
>;
|
218
|
-
|
219
|
-
type HTMLFormSubmitter = HTMLButtonElement | HTMLInputElement;
|
220
|
-
|
221
|
-
/**
|
222
|
-
* The primary form component of `remix-validated-form`.
|
223
|
-
*/
|
224
|
-
export function ValidatedForm<DataType, Subaction extends string | undefined>({
|
225
|
-
validator,
|
226
|
-
onSubmit,
|
227
|
-
children,
|
228
|
-
fetcher,
|
229
|
-
action,
|
230
|
-
defaultValues: unMemoizedDefaults,
|
231
|
-
formRef: formRefProp,
|
232
|
-
onReset,
|
233
|
-
subaction,
|
234
|
-
resetAfterSubmit = false,
|
235
|
-
disableFocusOnError,
|
236
|
-
method,
|
237
|
-
replace,
|
238
|
-
id,
|
239
|
-
preventScrollReset,
|
240
|
-
relative,
|
241
|
-
encType,
|
242
|
-
...rest
|
243
|
-
}: FormProps<DataType, Subaction>) {
|
244
|
-
const formId = useFormId(id);
|
245
|
-
const providedDefaultValues = useDeepEqualsMemo(unMemoizedDefaults);
|
246
|
-
const contextValue = useMemo<InternalFormContextValue>(
|
247
|
-
() => ({
|
248
|
-
formId,
|
249
|
-
action,
|
250
|
-
subaction,
|
251
|
-
defaultValuesProp: providedDefaultValues,
|
252
|
-
fetcher,
|
253
|
-
}),
|
254
|
-
[action, fetcher, formId, providedDefaultValues, subaction]
|
255
|
-
);
|
256
|
-
const backendError = useErrorResponseForForm(contextValue);
|
257
|
-
const backendDefaultValues = useDefaultValuesFromLoader(contextValue);
|
258
|
-
const hasActiveSubmission = useHasActiveFormSubmit(contextValue);
|
259
|
-
const formRef = useRef<HTMLFormElement>(null);
|
260
|
-
const Form = fetcher?.Form ?? RemixForm;
|
261
|
-
|
262
|
-
const submit = useSubmit();
|
263
|
-
const setFieldErrors = useSetFieldErrors(formId);
|
264
|
-
const setFieldError = useFormStore(formId, (state) => state.setFieldError);
|
265
|
-
const reset = useFormStore(formId, (state) => state.reset);
|
266
|
-
const startSubmit = useFormStore(formId, (state) => state.startSubmit);
|
267
|
-
const endSubmit = useFormStore(formId, (state) => state.endSubmit);
|
268
|
-
const syncFormProps = useFormStore(formId, (state) => state.syncFormProps);
|
269
|
-
const setFormElementInState = useFormStore(
|
270
|
-
formId,
|
271
|
-
(state) => state.setFormElement
|
272
|
-
);
|
273
|
-
const cleanupForm = useRootFormStore((state) => state.cleanupForm);
|
274
|
-
const registerForm = useRootFormStore((state) => state.registerForm);
|
275
|
-
|
276
|
-
const customFocusHandlers = useMultiValueMap<string, () => void>();
|
277
|
-
const registerReceiveFocus: SyncedFormProps["registerReceiveFocus"] =
|
278
|
-
useCallback(
|
279
|
-
(fieldName, handler) => {
|
280
|
-
customFocusHandlers().add(fieldName, handler);
|
281
|
-
return () => {
|
282
|
-
customFocusHandlers().remove(fieldName, handler);
|
283
|
-
};
|
284
|
-
},
|
285
|
-
[customFocusHandlers]
|
286
|
-
);
|
287
|
-
|
288
|
-
// TODO: all these hooks running at startup cause extra, unnecessary renders
|
289
|
-
// There must be a nice way to avoid this.
|
290
|
-
useLayoutEffect(() => {
|
291
|
-
registerForm(formId);
|
292
|
-
return () => cleanupForm(formId);
|
293
|
-
}, [cleanupForm, formId, registerForm]);
|
294
|
-
|
295
|
-
useLayoutEffect(() => {
|
296
|
-
syncFormProps({
|
297
|
-
action,
|
298
|
-
defaultValues: providedDefaultValues ?? backendDefaultValues ?? {},
|
299
|
-
subaction,
|
300
|
-
registerReceiveFocus,
|
301
|
-
validator,
|
302
|
-
});
|
303
|
-
}, [
|
304
|
-
action,
|
305
|
-
providedDefaultValues,
|
306
|
-
registerReceiveFocus,
|
307
|
-
subaction,
|
308
|
-
syncFormProps,
|
309
|
-
backendDefaultValues,
|
310
|
-
validator,
|
311
|
-
]);
|
312
|
-
|
313
|
-
useLayoutEffect(() => {
|
314
|
-
setFormElementInState(formRef.current);
|
315
|
-
}, [setFormElementInState]);
|
316
|
-
|
317
|
-
useEffect(() => {
|
318
|
-
setFieldErrors(backendError?.fieldErrors ?? {});
|
319
|
-
if (!disableFocusOnError && backendError?.fieldErrors) {
|
320
|
-
focusFirstInvalidInput(
|
321
|
-
backendError.fieldErrors,
|
322
|
-
customFocusHandlers(),
|
323
|
-
formRef.current!
|
324
|
-
);
|
325
|
-
}
|
326
|
-
}, [
|
327
|
-
backendError?.fieldErrors,
|
328
|
-
customFocusHandlers,
|
329
|
-
disableFocusOnError,
|
330
|
-
setFieldErrors,
|
331
|
-
setFieldError,
|
332
|
-
]);
|
333
|
-
|
334
|
-
useSubmitComplete(hasActiveSubmission, () => {
|
335
|
-
endSubmit();
|
336
|
-
});
|
337
|
-
|
338
|
-
const handleSubmit = async (
|
339
|
-
e: FormEvent<HTMLFormElement>,
|
340
|
-
target: typeof e.currentTarget,
|
341
|
-
nativeEvent: HTMLSubmitEvent["nativeEvent"]
|
342
|
-
) => {
|
343
|
-
startSubmit();
|
344
|
-
const submitter = nativeEvent.submitter as HTMLFormSubmitter | null;
|
345
|
-
const formMethod = (submitter?.formMethod as FormMethod) || method;
|
346
|
-
const formData = getDataFromForm(target);
|
347
|
-
if (submitter?.name) {
|
348
|
-
formData.append(submitter.name, submitter.value);
|
349
|
-
}
|
350
|
-
|
351
|
-
const result = await validator.validate(formData);
|
352
|
-
if (result.error) {
|
353
|
-
setFieldErrors(result.error.fieldErrors);
|
354
|
-
endSubmit();
|
355
|
-
if (!disableFocusOnError) {
|
356
|
-
focusFirstInvalidInput(
|
357
|
-
result.error.fieldErrors,
|
358
|
-
customFocusHandlers(),
|
359
|
-
formRef.current!
|
360
|
-
);
|
361
|
-
}
|
362
|
-
} else {
|
363
|
-
setFieldErrors({});
|
364
|
-
const eventProxy = formEventProxy(e);
|
365
|
-
await onSubmit?.(result.data as any, eventProxy);
|
366
|
-
if (eventProxy.defaultPrevented) {
|
367
|
-
endSubmit();
|
368
|
-
return;
|
369
|
-
}
|
370
|
-
|
371
|
-
const opts: SubmitOptions = {
|
372
|
-
method: formMethod,
|
373
|
-
replace,
|
374
|
-
preventScrollReset,
|
375
|
-
relative,
|
376
|
-
action,
|
377
|
-
encType: encType as FormEncType | undefined,
|
378
|
-
};
|
379
|
-
|
380
|
-
// We deviate from the Remix code here a bit because of our async submit.
|
381
|
-
// In Remix's `FormImpl`, they use `event.currentTarget` to get the form,
|
382
|
-
// but we already have the form in `formRef.current` so we can just use that.
|
383
|
-
// If we use `event.currentTarget` here, it will break because `currentTarget`
|
384
|
-
// will have changed since the start of the submission.
|
385
|
-
if (fetcher) fetcher.submit(formData, opts);
|
386
|
-
else submit(formData, opts);
|
387
|
-
}
|
388
|
-
};
|
389
|
-
|
390
|
-
return (
|
391
|
-
<Form
|
392
|
-
ref={mergeRefs([formRef, formRefProp])}
|
393
|
-
{...rest}
|
394
|
-
id={id}
|
395
|
-
action={action}
|
396
|
-
method={method}
|
397
|
-
encType={encType}
|
398
|
-
replace={replace}
|
399
|
-
preventScrollReset={preventScrollReset}
|
400
|
-
relative={relative}
|
401
|
-
onSubmit={(e) => {
|
402
|
-
e.preventDefault();
|
403
|
-
handleSubmit(
|
404
|
-
e,
|
405
|
-
e.currentTarget,
|
406
|
-
(e as unknown as HTMLSubmitEvent).nativeEvent
|
407
|
-
);
|
408
|
-
}}
|
409
|
-
onReset={(event) => {
|
410
|
-
onReset?.(event);
|
411
|
-
if (event.defaultPrevented) return;
|
412
|
-
reset();
|
413
|
-
}}
|
414
|
-
>
|
415
|
-
<InternalFormContext.Provider value={contextValue}>
|
416
|
-
<>
|
417
|
-
<FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
|
418
|
-
{subaction && (
|
419
|
-
<input type="hidden" value={subaction} name="subaction" />
|
420
|
-
)}
|
421
|
-
{id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
|
422
|
-
{children}
|
423
|
-
</>
|
424
|
-
</InternalFormContext.Provider>
|
425
|
-
</Form>
|
426
|
-
);
|
427
|
-
}
|
package/src/hooks.ts
DELETED
@@ -1,160 +0,0 @@
|
|
1
|
-
import { useEffect, useMemo } from "react";
|
2
|
-
import {
|
3
|
-
createGetInputProps,
|
4
|
-
GetInputProps,
|
5
|
-
ValidationBehaviorOptions,
|
6
|
-
} from "./internal/getInputProps";
|
7
|
-
import {
|
8
|
-
useInternalFormContext,
|
9
|
-
useFieldTouched,
|
10
|
-
useFieldError,
|
11
|
-
useFieldDefaultValue,
|
12
|
-
useClearError,
|
13
|
-
useInternalIsSubmitting,
|
14
|
-
useInternalIsValid,
|
15
|
-
useInternalHasBeenSubmitted,
|
16
|
-
useRegisterReceiveFocus,
|
17
|
-
useSmartValidate,
|
18
|
-
} from "./internal/hooks";
|
19
|
-
import {
|
20
|
-
useControllableValue,
|
21
|
-
useUpdateControllableValue,
|
22
|
-
} from "./internal/state/controlledFields";
|
23
|
-
|
24
|
-
/**
|
25
|
-
* Returns whether or not the parent form is currently being submitted.
|
26
|
-
* This is different from Remix's `useNavigation()` in that it
|
27
|
-
* is aware of what form it's in and when _that_ form is being submitted.
|
28
|
-
*
|
29
|
-
* @param formId
|
30
|
-
*/
|
31
|
-
export const useIsSubmitting = (formId?: string) => {
|
32
|
-
const formContext = useInternalFormContext(formId, "useIsSubmitting");
|
33
|
-
return useInternalIsSubmitting(formContext.formId);
|
34
|
-
};
|
35
|
-
|
36
|
-
/**
|
37
|
-
* Returns whether or not the current form is valid.
|
38
|
-
*
|
39
|
-
* @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
|
40
|
-
*/
|
41
|
-
export const useIsValid = (formId?: string) => {
|
42
|
-
const formContext = useInternalFormContext(formId, "useIsValid");
|
43
|
-
return useInternalIsValid(formContext.formId);
|
44
|
-
};
|
45
|
-
|
46
|
-
export type FieldProps = {
|
47
|
-
/**
|
48
|
-
* The validation error message if there is one.
|
49
|
-
*/
|
50
|
-
error?: string;
|
51
|
-
/**
|
52
|
-
* Clears the error message.
|
53
|
-
*/
|
54
|
-
clearError: () => void;
|
55
|
-
/**
|
56
|
-
* Validates the field.
|
57
|
-
*/
|
58
|
-
validate: () => void;
|
59
|
-
/**
|
60
|
-
* The default value of the field, if there is one.
|
61
|
-
*/
|
62
|
-
defaultValue?: any;
|
63
|
-
/**
|
64
|
-
* Whether or not the field has been touched.
|
65
|
-
*/
|
66
|
-
touched: boolean;
|
67
|
-
/**
|
68
|
-
* Helper to set the touched state of the field.
|
69
|
-
*/
|
70
|
-
setTouched: (touched: boolean) => void;
|
71
|
-
/**
|
72
|
-
* Helper to get all the props necessary for a regular input.
|
73
|
-
*/
|
74
|
-
getInputProps: GetInputProps;
|
75
|
-
};
|
76
|
-
|
77
|
-
/**
|
78
|
-
* Provides the data and helpers necessary to set up a field.
|
79
|
-
*/
|
80
|
-
export const useField = (
|
81
|
-
name: string,
|
82
|
-
options?: {
|
83
|
-
/**
|
84
|
-
* Allows you to configure a custom function that will be called
|
85
|
-
* when the input needs to receive focus due to a validation error.
|
86
|
-
* This is useful for custom components that use a hidden input.
|
87
|
-
*/
|
88
|
-
handleReceiveFocus?: () => void;
|
89
|
-
/**
|
90
|
-
* Allows you to specify when a field gets validated (when using getInputProps)
|
91
|
-
*/
|
92
|
-
validationBehavior?: Partial<ValidationBehaviorOptions>;
|
93
|
-
/**
|
94
|
-
* The formId of the form you want to use.
|
95
|
-
* This is not necesary if the input is used inside a form.
|
96
|
-
*/
|
97
|
-
formId?: string;
|
98
|
-
}
|
99
|
-
): FieldProps => {
|
100
|
-
const { formId: providedFormId, handleReceiveFocus } = options ?? {};
|
101
|
-
const formContext = useInternalFormContext(providedFormId, "useField");
|
102
|
-
|
103
|
-
const defaultValue = useFieldDefaultValue(name, formContext);
|
104
|
-
const [touched, setTouched] = useFieldTouched(name, formContext);
|
105
|
-
const error = useFieldError(name, formContext);
|
106
|
-
const clearError = useClearError(formContext);
|
107
|
-
|
108
|
-
const hasBeenSubmitted = useInternalHasBeenSubmitted(formContext.formId);
|
109
|
-
const smartValidate = useSmartValidate(formContext.formId);
|
110
|
-
const registerReceiveFocus = useRegisterReceiveFocus(formContext.formId);
|
111
|
-
|
112
|
-
useEffect(() => {
|
113
|
-
if (handleReceiveFocus)
|
114
|
-
return registerReceiveFocus(name, handleReceiveFocus);
|
115
|
-
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
116
|
-
|
117
|
-
const field = useMemo<FieldProps>(() => {
|
118
|
-
const helpers = {
|
119
|
-
error,
|
120
|
-
clearError: () => clearError(name),
|
121
|
-
validate: () => smartValidate({ alwaysIncludeErrorsFromFields: [name] }),
|
122
|
-
defaultValue,
|
123
|
-
touched,
|
124
|
-
setTouched,
|
125
|
-
};
|
126
|
-
const getInputProps = createGetInputProps({
|
127
|
-
...helpers,
|
128
|
-
name,
|
129
|
-
hasBeenSubmitted,
|
130
|
-
validationBehavior: options?.validationBehavior,
|
131
|
-
});
|
132
|
-
return {
|
133
|
-
...helpers,
|
134
|
-
getInputProps,
|
135
|
-
};
|
136
|
-
}, [
|
137
|
-
error,
|
138
|
-
clearError,
|
139
|
-
defaultValue,
|
140
|
-
touched,
|
141
|
-
setTouched,
|
142
|
-
name,
|
143
|
-
hasBeenSubmitted,
|
144
|
-
options?.validationBehavior,
|
145
|
-
smartValidate,
|
146
|
-
]);
|
147
|
-
|
148
|
-
return field;
|
149
|
-
};
|
150
|
-
|
151
|
-
export const useControlField = <T>(name: string, formId?: string) => {
|
152
|
-
const context = useInternalFormContext(formId, "useControlField");
|
153
|
-
const [value, setValue] = useControllableValue(context, name);
|
154
|
-
return [value as T, setValue as (value: T) => void] as const;
|
155
|
-
};
|
156
|
-
|
157
|
-
export const useUpdateControlledField = (formId?: string) => {
|
158
|
-
const context = useInternalFormContext(formId, "useControlField");
|
159
|
-
return useUpdateControllableValue(context.formId);
|
160
|
-
};
|
package/src/index.ts
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
export * from "./hooks";
|
2
|
-
export * from "./server";
|
3
|
-
export * from "./ValidatedForm";
|
4
|
-
export * from "./validation/types";
|
5
|
-
export * from "./validation/createValidator";
|
6
|
-
export * from "./userFacingFormContext";
|
7
|
-
export {
|
8
|
-
FieldArray,
|
9
|
-
useFieldArray,
|
10
|
-
type FieldArrayProps,
|
11
|
-
type FieldArrayHelpers,
|
12
|
-
} from "./internal/state/fieldArray";
|
@@ -1,44 +0,0 @@
|
|
1
|
-
import { useCallback, useRef } from "react";
|
2
|
-
|
3
|
-
export class MultiValueMap<Key, Value> {
|
4
|
-
private dict: Map<Key, Value[]> = new Map();
|
5
|
-
|
6
|
-
add = (key: Key, value: Value) => {
|
7
|
-
if (this.dict.has(key)) {
|
8
|
-
this.dict.get(key)!.push(value);
|
9
|
-
} else {
|
10
|
-
this.dict.set(key, [value]);
|
11
|
-
}
|
12
|
-
};
|
13
|
-
|
14
|
-
delete = (key: Key) => {
|
15
|
-
this.dict.delete(key);
|
16
|
-
};
|
17
|
-
|
18
|
-
remove = (key: Key, value: Value) => {
|
19
|
-
if (!this.dict.has(key)) return;
|
20
|
-
const array = this.dict.get(key)!;
|
21
|
-
const index = array.indexOf(value);
|
22
|
-
if (index !== -1) array.splice(index, 1);
|
23
|
-
if (array.length === 0) this.dict.delete(key);
|
24
|
-
};
|
25
|
-
|
26
|
-
getAll = (key: Key): Value[] => {
|
27
|
-
return this.dict.get(key) ?? [];
|
28
|
-
};
|
29
|
-
|
30
|
-
entries = (): IterableIterator<[Key, Value[]]> => this.dict.entries();
|
31
|
-
|
32
|
-
values = (): IterableIterator<Value[]> => this.dict.values();
|
33
|
-
|
34
|
-
has = (key: Key): boolean => this.dict.has(key);
|
35
|
-
}
|
36
|
-
|
37
|
-
export const useMultiValueMap = <Key, Value>() => {
|
38
|
-
const ref = useRef<MultiValueMap<Key, Value> | null>(null);
|
39
|
-
return useCallback(() => {
|
40
|
-
if (ref.current) return ref.current;
|
41
|
-
ref.current = new MultiValueMap();
|
42
|
-
return ref.current;
|
43
|
-
}, []);
|
44
|
-
};
|
package/src/internal/flatten.ts
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
import { setPath } from "set-get";
|
2
|
-
import { MultiValueMap } from "./MultiValueMap";
|
3
|
-
|
4
|
-
export const objectFromPathEntries = (entries: [string, any][]) => {
|
5
|
-
const map = new MultiValueMap<string, any>();
|
6
|
-
entries.forEach(([key, value]) => map.add(key, value));
|
7
|
-
return [...map.entries()].reduce(
|
8
|
-
(acc, [key, value]) =>
|
9
|
-
setPath(acc, key, value.length === 1 ? value[0] : value),
|
10
|
-
{} as Record<string, any>
|
11
|
-
);
|
12
|
-
};
|
@@ -1,13 +0,0 @@
|
|
1
|
-
import { FetcherWithComponents } from "@remix-run/react";
|
2
|
-
import { createContext } from "react";
|
3
|
-
|
4
|
-
export type InternalFormContextValue = {
|
5
|
-
formId: string | symbol;
|
6
|
-
action?: string;
|
7
|
-
subaction?: string;
|
8
|
-
defaultValuesProp?: { [fieldName: string]: any };
|
9
|
-
fetcher?: FetcherWithComponents<unknown>;
|
10
|
-
};
|
11
|
-
|
12
|
-
export const InternalFormContext =
|
13
|
-
createContext<InternalFormContextValue | null>(null);
|