@4riders/reform 3.0.24
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/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/index.d.ts +2715 -0
- package/dist/index.es.js +1715 -0
- package/dist/index.es.js.map +1 -0
- package/package.json +70 -0
- package/src/index.ts +90 -0
- package/src/reform/ArrayHelper.ts +164 -0
- package/src/reform/Form.tsx +81 -0
- package/src/reform/FormManager.ts +494 -0
- package/src/reform/Reform.ts +15 -0
- package/src/reform/components/BaseCheckboxField.tsx +72 -0
- package/src/reform/components/BaseDateField.tsx +84 -0
- package/src/reform/components/BaseRadioField.tsx +72 -0
- package/src/reform/components/BaseSelectField.tsx +103 -0
- package/src/reform/components/BaseTextAreaField.tsx +87 -0
- package/src/reform/components/BaseTextField.tsx +135 -0
- package/src/reform/components/InputHTMLProps.tsx +89 -0
- package/src/reform/observers/observer.ts +131 -0
- package/src/reform/observers/observerPath.ts +327 -0
- package/src/reform/observers/useObservers.ts +232 -0
- package/src/reform/useForm.ts +246 -0
- package/src/reform/useFormContext.tsx +37 -0
- package/src/reform/useFormField.ts +75 -0
- package/src/reform/useRender.ts +12 -0
- package/src/yop/MessageProvider.ts +204 -0
- package/src/yop/Metadata.ts +304 -0
- package/src/yop/ObjectsUtil.ts +811 -0
- package/src/yop/TypesUtil.ts +148 -0
- package/src/yop/ValidationContext.ts +207 -0
- package/src/yop/Yop.ts +430 -0
- package/src/yop/constraints/CommonConstraints.ts +124 -0
- package/src/yop/constraints/Constraint.ts +135 -0
- package/src/yop/constraints/MinMaxConstraints.ts +53 -0
- package/src/yop/constraints/OneOfConstraint.ts +40 -0
- package/src/yop/constraints/TestConstraint.ts +176 -0
- package/src/yop/decorators/array.ts +157 -0
- package/src/yop/decorators/boolean.ts +69 -0
- package/src/yop/decorators/date.ts +73 -0
- package/src/yop/decorators/email.ts +66 -0
- package/src/yop/decorators/file.ts +69 -0
- package/src/yop/decorators/id.ts +35 -0
- package/src/yop/decorators/ignored.ts +40 -0
- package/src/yop/decorators/instance.ts +110 -0
- package/src/yop/decorators/number.ts +73 -0
- package/src/yop/decorators/string.ts +90 -0
- package/src/yop/decorators/test.ts +41 -0
- package/src/yop/decorators/time.ts +112 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
2
|
+
import { instance } from "../yop/decorators/instance"
|
|
3
|
+
import { Path } from "../yop/ObjectsUtil"
|
|
4
|
+
import { isPromise } from "../yop/TypesUtil"
|
|
5
|
+
import { Group } from "../yop/ValidationContext"
|
|
6
|
+
import { FormManager, InternalFormManager } from "./FormManager"
|
|
7
|
+
import { useRender } from "./useRender"
|
|
8
|
+
import { useObservers } from "./observers/useObservers"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration options for the useForm hook.
|
|
12
|
+
* @template T - The type of the form values.
|
|
13
|
+
* @category Form Management
|
|
14
|
+
*/
|
|
15
|
+
export type FormConfig<T extends object | any[] | null | undefined> = {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initial values for the form. Can be an object, a function returning an object, or a function returning a promise that
|
|
19
|
+
* resolves to an object. Initial values are cloned and stored internally the first time they are neither `null` nor `undefined`.
|
|
20
|
+
* If a function is provided, it will be called on the first render and whenever the config changes, allowing for dynamic
|
|
21
|
+
* initial values. If the function returns a promise, the form will be in a pending state until the promise resolves, at
|
|
22
|
+
* which point the initial values will be set and the form will re-render.
|
|
23
|
+
*
|
|
24
|
+
* @see {@link FormManager.initialValuesPending}
|
|
25
|
+
* @see {@link FormConfig.initialValuesConverter}
|
|
26
|
+
*/
|
|
27
|
+
readonly initialValues?: T | (() => T) | (() => Promise<T>) | null
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Converter function for initial values. This function is called with the initial values whenever they become neither
|
|
31
|
+
* `null` nor `undefined`. It allows for transformation or normalization of the initial values before they are set in the form.
|
|
32
|
+
* @param values - The initial values.
|
|
33
|
+
* @returns The transformed initial values.
|
|
34
|
+
*/
|
|
35
|
+
readonly initialValuesConverter?: (values: T) => T
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The validation schema for the form. This can be a schema object created with the `instance` or `array` decorator.
|
|
39
|
+
* It defines the rules for validating the form values.
|
|
40
|
+
*
|
|
41
|
+
* Example usage:
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const form = useForm({
|
|
44
|
+
* initialValues: new Person(),
|
|
45
|
+
* validationSchema: instance({ of: Person, required: true }),
|
|
46
|
+
* })
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
readonly validationSchema?: ((_: unknown, context: ClassFieldDecoratorContext<unknown, T>) => void)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Path or paths to validate. This can be a single path string or an array of path strings. If specified, only the values
|
|
53
|
+
* at these paths will be validated. If not specified, the entire form values will be validated.
|
|
54
|
+
*/
|
|
55
|
+
readonly validationPath?: string | string[]
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validation groups to use during validation. This can be a single group or an array of groups. If specified, the validation
|
|
59
|
+
* rules associated with these groups will be applied. If not specified, only the validation rules of the default group will
|
|
60
|
+
* be applied.
|
|
61
|
+
*
|
|
62
|
+
* For example:
|
|
63
|
+
* ```tsx
|
|
64
|
+
* // will apply validation rules of "group1":
|
|
65
|
+
* validationGroups: "group1"
|
|
66
|
+
* // will apply validation rules of "group1" and "group2":
|
|
67
|
+
* validationGroups: ["group1", "group2"]
|
|
68
|
+
* // will apply validation rules of the default group and "group2":
|
|
69
|
+
* validationGroups: [undefined, "group2"]
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
readonly validationGroups?: Group
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Function to determine if a path should be ignored during validation. This function is called with the path being
|
|
76
|
+
* validated and the form manager instance. If it returns `true`, the path will be ignored and no validation will be
|
|
77
|
+
* performed for it.
|
|
78
|
+
* @param path - The path being validated.
|
|
79
|
+
* @param form - The form manager instance.
|
|
80
|
+
* @returns `true` if the path should be ignored, `false` otherwise.
|
|
81
|
+
*/
|
|
82
|
+
readonly ignore?: (path: Path, form: FormManager<T>) => boolean
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Function to determine if the form can be submitted. This function is called with the form manager instance when a submit
|
|
86
|
+
* is attempted. If it returns `true`, the form will be submitted and the `onSubmit` callback will be called. If it returns
|
|
87
|
+
* `false`, the submit will be aborted and the `onSubmit` callback will not be called.
|
|
88
|
+
* @param form - The form manager instance.
|
|
89
|
+
* @returns `true` if the form can be submitted, `false` otherwise.
|
|
90
|
+
*/
|
|
91
|
+
readonly submitGuard?: (form: FormManager<T>) => boolean
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Callback for form submission. This function is called with the form manager instance when the form is submitted and the
|
|
95
|
+
* `submitGuard` (if provided) returns `true`. It is responsible for handling the form submission logic, such as sending
|
|
96
|
+
* the form values to a server or updating application state. The {@link FormManager.submitting} is automatically set to
|
|
97
|
+
* `true` while this function is called. It is the responsibility of the caller to set it back to `false` when the
|
|
98
|
+
* submission process is complete (see {@link FormManager.setSubmitting}).
|
|
99
|
+
*
|
|
100
|
+
* Example usage:
|
|
101
|
+
* ```tsx
|
|
102
|
+
* const form = useForm({
|
|
103
|
+
* initialValues: new Person(),
|
|
104
|
+
* validationSchema: instance({ of: Person, required: true }),
|
|
105
|
+
* onSubmit: (form) => {
|
|
106
|
+
* console.log("Form submitted with values:", form.values)
|
|
107
|
+
* try {
|
|
108
|
+
* // perform submission logic here
|
|
109
|
+
* }
|
|
110
|
+
* finally {
|
|
111
|
+
* form.setSubmitting(false)
|
|
112
|
+
* }
|
|
113
|
+
* }
|
|
114
|
+
* })
|
|
115
|
+
* ```
|
|
116
|
+
* @param form - The form manager instance.
|
|
117
|
+
* @returns void
|
|
118
|
+
*/
|
|
119
|
+
readonly onSubmit?: (form: FormManager<T>) => void
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Whether to dispatch events for observer propagation. If `true`, when a value changes, an event will be dispatched that can
|
|
123
|
+
* be listened to by observers to react to value changes. Default is `true`.
|
|
124
|
+
*/
|
|
125
|
+
readonly dispatchEvent?: boolean
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Type for a class constructor of a model.
|
|
130
|
+
* @category Form Management
|
|
131
|
+
*/
|
|
132
|
+
export type Model<T> = new (...args: any) => NonNullable<T>
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* ## First overload signature
|
|
137
|
+
*
|
|
138
|
+
* React hook to create and manage a form with all configuration options available in {@link FormConfig}. This overload allows for the most flexible
|
|
139
|
+
* usage of the `useForm` hook, with full control over initial values, validation schema, submission logic, and more.
|
|
140
|
+
*
|
|
141
|
+
* However, it doesn't register automatically observers listeners, and you need to use the {@link useObservers} hook manually to register observers on
|
|
142
|
+
* the form manager instance, as shown in the example below.
|
|
143
|
+
*
|
|
144
|
+
* Example usage:
|
|
145
|
+
* ```tsx
|
|
146
|
+
* const form = useForm({
|
|
147
|
+
* initialValues: new Person(),
|
|
148
|
+
* validationSchema: instance({ of: Person, required: true }),
|
|
149
|
+
* onSubmit: (form) => {
|
|
150
|
+
* console.log("Form submitted with values:", form.values)
|
|
151
|
+
* form.setSubmitting(false)
|
|
152
|
+
* }
|
|
153
|
+
* })
|
|
154
|
+
* // Optional, if observers are used in the form:
|
|
155
|
+
* useObservers(Person, form)
|
|
156
|
+
* ```
|
|
157
|
+
* @overload
|
|
158
|
+
* @template T - The type of the form values.
|
|
159
|
+
* @param config - The form configuration object.
|
|
160
|
+
* @param deps - Optional dependency list for memoization of the form manager.
|
|
161
|
+
* @returns The form manager instance.
|
|
162
|
+
* @category Form Management
|
|
163
|
+
*/
|
|
164
|
+
export function useForm<T extends object | null | undefined>(config: FormConfig<T>, deps?: React.DependencyList): FormManager<T>
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* ## Second overload signature
|
|
168
|
+
*
|
|
169
|
+
* React hook to create and manage a form with validation, and automatic observer support. This overload allows for a simpler syntax. The initial values
|
|
170
|
+
* will be created by instantiating the provided model class, and the validation schema will be automatically generated using the `instance` decorator with
|
|
171
|
+
* the provided model class and `required: true`.
|
|
172
|
+
*
|
|
173
|
+
* There is no need to use {@link useObservers} here, observers will be automatically registered on the form manager instance
|
|
174
|
+
* for the provided model class. The code example below is strictly equivalent to the one in the other overload signature,
|
|
175
|
+
* but with a simpler syntax.
|
|
176
|
+
*
|
|
177
|
+
* Example usage:
|
|
178
|
+
* ```tsx
|
|
179
|
+
* const form = useForm(Person, (form) => {
|
|
180
|
+
* console.log("Form submitted with values:", form.values)
|
|
181
|
+
* form.setSubmitting(false)
|
|
182
|
+
* })
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @overload
|
|
186
|
+
* @template T - The type of the form values.
|
|
187
|
+
* @param model - The model class constructor.
|
|
188
|
+
* @param onSubmit - Callback for form submission.
|
|
189
|
+
* @param deps - Optional dependency list for memoization of the form manager.
|
|
190
|
+
* @returns The form manager instance.
|
|
191
|
+
* @category Form Management
|
|
192
|
+
*/
|
|
193
|
+
export function useForm<T extends object | null | undefined>(model: Model<T>, onSubmit: (form: FormManager<T>) => void, deps?: React.DependencyList): FormManager<T>
|
|
194
|
+
|
|
195
|
+
/*
|
|
196
|
+
* Implementation of the useForm hook. Handles both config and model overloads, supports async initial values,
|
|
197
|
+
* and manages form state, validation, and observer eventing. See the overload signatures for usage details.
|
|
198
|
+
*/
|
|
199
|
+
export function useForm(configOrModel: any, onSubmitOrDeps?: any, deps: React.DependencyList = []) {
|
|
200
|
+
|
|
201
|
+
const model = typeof configOrModel === "function" ? configOrModel : undefined
|
|
202
|
+
const render = useRender()
|
|
203
|
+
|
|
204
|
+
deps = Array.isArray(onSubmitOrDeps) ? onSubmitOrDeps : deps
|
|
205
|
+
const manager = useMemo(() => {
|
|
206
|
+
const newManager = new InternalFormManager(render)
|
|
207
|
+
|
|
208
|
+
if (typeof configOrModel === "function") {
|
|
209
|
+
configOrModel = {
|
|
210
|
+
initialValues: new configOrModel(),
|
|
211
|
+
validationSchema: instance({ of: configOrModel, required: true }),
|
|
212
|
+
onSubmit: onSubmitOrDeps as ((form: FormManager<any>) => void),
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (typeof configOrModel.initialValues === "function") {
|
|
216
|
+
let initialValues = configOrModel.initialValues()
|
|
217
|
+
if (isPromise(initialValues)) {
|
|
218
|
+
newManager.initialValuesPending = true
|
|
219
|
+
initialValues.then((value: any) => {
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
configOrModel = { ...newManager.config, initialValues: value }
|
|
222
|
+
newManager.onRender(configOrModel)
|
|
223
|
+
newManager.commitInitialValues()
|
|
224
|
+
newManager.initialValuesPending = false
|
|
225
|
+
render()
|
|
226
|
+
}, 0)
|
|
227
|
+
})
|
|
228
|
+
initialValues = newManager.initialValues
|
|
229
|
+
}
|
|
230
|
+
configOrModel = { ...configOrModel, initialValues }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
newManager.onRender(configOrModel)
|
|
234
|
+
return newManager
|
|
235
|
+
}, deps)
|
|
236
|
+
|
|
237
|
+
// We need this code to normalize configOrModel when useMemo doesn't re-run.
|
|
238
|
+
if (typeof configOrModel === "function")
|
|
239
|
+
configOrModel = { ...manager.config, onSubmit: onSubmitOrDeps as ((form: FormManager<any>) => void) }
|
|
240
|
+
else if (typeof configOrModel.initialValues === "function")
|
|
241
|
+
configOrModel = { ...configOrModel, initialValues: manager.initialValues }
|
|
242
|
+
|
|
243
|
+
manager.onRender(configOrModel)
|
|
244
|
+
useObservers(model, manager)
|
|
245
|
+
return manager
|
|
246
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { FormManager } from "./FormManager"
|
|
3
|
+
import { Form } from "./Form"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* React context for providing a FormManager instance to descendant components.
|
|
7
|
+
* @ignore
|
|
8
|
+
*/
|
|
9
|
+
export const FormContext = React.createContext<FormManager<unknown> | null>(null)
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* React hook to access the current {@link FormManager} from context. This hook should be used within a component that is a descendant of a {@link Form} component,
|
|
13
|
+
* which provides the FormManager via context. The generic type parameter `T` can be used to specify the type of the form values managed by the FormManager,
|
|
14
|
+
* allowing for type-safe access to form values.
|
|
15
|
+
*
|
|
16
|
+
* Example usage:
|
|
17
|
+
* ```tsx
|
|
18
|
+
* function MyFormComponent() {
|
|
19
|
+
* const form = useFormContext<MyFormValues>()
|
|
20
|
+
* // use form to access values, statuses, etc.
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* const form = useForm(MyFormModel, onSubmit)
|
|
24
|
+
* return (
|
|
25
|
+
* <Form form={form} autoComplete="off" noValidate disabled={form.submitting}>
|
|
26
|
+
* <MyFormComponent />
|
|
27
|
+
* </Form>
|
|
28
|
+
* )
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @template T - The type of the form values managed by the FormManager.
|
|
32
|
+
* @returns The {@link FormManager} instance from context.
|
|
33
|
+
* @category Form Management
|
|
34
|
+
*/
|
|
35
|
+
export function useFormContext<T = unknown>() {
|
|
36
|
+
return React.useContext(FormContext)! as FormManager<T>
|
|
37
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { isPromise } from "../yop/TypesUtil";
|
|
3
|
+
import { ValidationStatus } from "../yop/ValidationContext";
|
|
4
|
+
import { ResolvedConstraints } from "../yop/Yop";
|
|
5
|
+
import { FormManager } from "./FormManager";
|
|
6
|
+
import { useFormContext } from "./useFormContext";
|
|
7
|
+
import { useRender } from "./useRender";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents the state of a form field, including value, validation, and metadata.
|
|
11
|
+
* @template Value - The type of the field value.
|
|
12
|
+
* @template MinMax - The type for min/max constraints.
|
|
13
|
+
* @template Root - The type of the root form values.
|
|
14
|
+
* @category Form Management
|
|
15
|
+
*/
|
|
16
|
+
export type FieldState<Value, MinMax, Root = any> = {
|
|
17
|
+
/** The current value of the field. */
|
|
18
|
+
value: Value | undefined
|
|
19
|
+
/** Whether the field has been touched. */
|
|
20
|
+
touched: boolean
|
|
21
|
+
/** The validation status of the field, if any. See {@link ValidationStatus} for details. */
|
|
22
|
+
status?: ValidationStatus
|
|
23
|
+
/** The form manager instance. See {@link FormManager} for details. */
|
|
24
|
+
form: FormManager<Root>
|
|
25
|
+
/** Function to trigger a re-render of the component that called {@link useFormField} to get this field state. */
|
|
26
|
+
render: () => void
|
|
27
|
+
/** The resolved constraints for the field, if any. See {@link ResolvedConstraints} for details. */
|
|
28
|
+
constraints?: ResolvedConstraints<MinMax>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* React hook to access and manage the state of a form field, including value, validation status, and constraints.
|
|
33
|
+
* Handles async validation and triggers re-renders as needed.
|
|
34
|
+
*
|
|
35
|
+
* Example usage:
|
|
36
|
+
* ```tsx
|
|
37
|
+
* function MyTextField(props: { path: string }) {
|
|
38
|
+
* const { constraints, status, value, form } = useFormField<string, number>(props.path)
|
|
39
|
+
* // render input with value, display validation status, etc.
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @template Value - The type of the field value.
|
|
44
|
+
* @template MinMax - The type for min/max constraints.
|
|
45
|
+
* @template Root - The type of the root form values.
|
|
46
|
+
* @param name - The field name or path.
|
|
47
|
+
* @param unsafeMetadata - Whether to use unsafe metadata for constraints.
|
|
48
|
+
* @returns The current state of the field. See {@link FieldState} for details.
|
|
49
|
+
* @category Form Management
|
|
50
|
+
*/
|
|
51
|
+
export function useFormField<Value, MinMax, Root = any>(name: string, unsafeMetadata = false): FieldState<Value, MinMax, Root> {
|
|
52
|
+
const render = useRender()
|
|
53
|
+
const form = useFormContext<Root>()
|
|
54
|
+
const promiseRef = useRef<Promise<unknown>>(undefined)
|
|
55
|
+
|
|
56
|
+
const status = form.statuses.get(name)
|
|
57
|
+
if (status?.level === "pending" && isPromise(status.constraint) && promiseRef.current !== status.constraint) {
|
|
58
|
+
promiseRef.current = status.constraint
|
|
59
|
+
status.constraint.finally(() => {
|
|
60
|
+
if (promiseRef.current === status.constraint) {
|
|
61
|
+
form.updateAsyncStatus(name)
|
|
62
|
+
form.render()
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
value: form.getValue<Value>(name),
|
|
69
|
+
touched: form.isTouched(name),
|
|
70
|
+
status,
|
|
71
|
+
form,
|
|
72
|
+
render,
|
|
73
|
+
constraints: form.constraintsAt(name, unsafeMetadata),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useReducer } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* React hook that returns a function to force a component re-render.
|
|
5
|
+
* Useful for triggering updates in custom hooks or non-stateful logic.
|
|
6
|
+
*
|
|
7
|
+
* @returns A function that, when called, forces the component to re-render.
|
|
8
|
+
* @category Form Management
|
|
9
|
+
*/
|
|
10
|
+
export function useRender(): () => void {
|
|
11
|
+
return useReducer(() => ({}), {})[1] as () => void
|
|
12
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { ConstraintMessage } from "./constraints/Constraint";
|
|
2
|
+
import { InternalValidationContext, Level, ValidationContext } from "./ValidationContext";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interface for providing localized validation messages.
|
|
6
|
+
* @category Localization
|
|
7
|
+
*/
|
|
8
|
+
export interface MessageProvider {
|
|
9
|
+
/**
|
|
10
|
+
* The locale identifier (e.g., 'en-US', 'fr-FR').
|
|
11
|
+
*/
|
|
12
|
+
readonly locale: string
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns a localized message for a given validation context and code.
|
|
16
|
+
* @param context - The validation context.
|
|
17
|
+
* @param code - The message code.
|
|
18
|
+
* @param constraint - The constraint value.
|
|
19
|
+
* @param message - An optional custom message.
|
|
20
|
+
* @param level - The validation level (e.g., 'error', 'pending').
|
|
21
|
+
* @returns The resolved message.
|
|
22
|
+
*/
|
|
23
|
+
getMessage(context: InternalValidationContext<unknown>, code: string, constraint: any, message: ConstraintMessage | undefined, level: Level): ConstraintMessage
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Formats a value for display in a localized message, handling numbers, dates, and arrays.
|
|
28
|
+
* @param value - The value to format.
|
|
29
|
+
* @param numberFormat - The number formatter.
|
|
30
|
+
* @param dateFormat - The date formatter.
|
|
31
|
+
* @param listFormat - The list formatter.
|
|
32
|
+
* @returns The formatted string.
|
|
33
|
+
* @ignore
|
|
34
|
+
*/
|
|
35
|
+
function format(value: any, numberFormat: Intl.NumberFormat, dateFormat: Intl.DateTimeFormat, listFormat: Intl.ListFormat): string {
|
|
36
|
+
return (
|
|
37
|
+
typeof value === "number" ? numberFormat.format(value) :
|
|
38
|
+
value instanceof Date ? dateFormat.format(value) :
|
|
39
|
+
Array.isArray(value) ? listFormat.format(value.map(item => format(item, numberFormat, dateFormat, listFormat))) :
|
|
40
|
+
String(value)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Properties passed to a message function for formatting.
|
|
46
|
+
* @category Localization
|
|
47
|
+
*/
|
|
48
|
+
export type MessageProps = {
|
|
49
|
+
context: ValidationContext<unknown>
|
|
50
|
+
code: string
|
|
51
|
+
constraint: {
|
|
52
|
+
raw: any
|
|
53
|
+
formatted: string
|
|
54
|
+
plural?: Intl.LDMLPluralRule
|
|
55
|
+
}
|
|
56
|
+
level: Level
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Function type for generating a message from message properties.
|
|
61
|
+
* @see {@link MessageProps}
|
|
62
|
+
* @category Localization
|
|
63
|
+
*/
|
|
64
|
+
export type MessageFunction = (props: MessageProps) => string
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Basic implementation of MessageProvider for localized validation messages.
|
|
68
|
+
* @category Localization
|
|
69
|
+
*/
|
|
70
|
+
export class BasicMessageProvider implements MessageProvider {
|
|
71
|
+
|
|
72
|
+
private readonly numberFormat: Intl.NumberFormat
|
|
73
|
+
private readonly dateFormat: Intl.DateTimeFormat
|
|
74
|
+
private readonly listFormat: Intl.ListFormat
|
|
75
|
+
private readonly pluralRules: Intl.PluralRules
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Map of message codes to message functions.
|
|
79
|
+
*/
|
|
80
|
+
readonly messages: Map<string, MessageFunction>
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a new BasicMessageProvider for a given locale and message entries.
|
|
84
|
+
* @param locale - The locale identifier.
|
|
85
|
+
* @param entries - Optional array of [code, message function] pairs.
|
|
86
|
+
*/
|
|
87
|
+
constructor(readonly locale: string, entries?: (readonly [string, MessageFunction])[]) {
|
|
88
|
+
this.numberFormat = new Intl.NumberFormat(this.locale)
|
|
89
|
+
this.dateFormat = new Intl.DateTimeFormat(this.locale)
|
|
90
|
+
this.listFormat = new Intl.ListFormat(this.locale, { type: "disjunction" })
|
|
91
|
+
this.pluralRules = new Intl.PluralRules(this.locale)
|
|
92
|
+
|
|
93
|
+
this.messages = new Map<string, MessageFunction>(entries)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @inheritdoc
|
|
98
|
+
*/
|
|
99
|
+
getMessage(context: InternalValidationContext<unknown>, code: string, constraint: any, message: ConstraintMessage | undefined, level: Level): ConstraintMessage {
|
|
100
|
+
if (message != null)
|
|
101
|
+
return message
|
|
102
|
+
|
|
103
|
+
const messageFunction = this.messages.get(`${ context.kind }.${ code }`) ?? this.messages.get(code)
|
|
104
|
+
if (messageFunction == null)
|
|
105
|
+
return `Unexpected error: ${ context.kind }.${ code }`
|
|
106
|
+
|
|
107
|
+
return messageFunction({
|
|
108
|
+
context,
|
|
109
|
+
code,
|
|
110
|
+
constraint: {
|
|
111
|
+
raw: constraint,
|
|
112
|
+
formatted: format(constraint, this.numberFormat, this.dateFormat, this.listFormat),
|
|
113
|
+
plural: typeof constraint === "number" ? this.pluralRules.select(constraint) : undefined
|
|
114
|
+
},
|
|
115
|
+
level
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns the plural suffix 's' if the plural rule is not 'one'.
|
|
122
|
+
* @param plural - The plural rule.
|
|
123
|
+
* @returns 's' if plural, otherwise an empty string.
|
|
124
|
+
* @ignore
|
|
125
|
+
*/
|
|
126
|
+
function s(plural?: Intl.LDMLPluralRule): string {
|
|
127
|
+
return plural == null || plural === "one" ? "" : "s"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* English (US) message provider for validation messages.
|
|
132
|
+
* @category Localization
|
|
133
|
+
*/
|
|
134
|
+
export const messageProvider_en_US = new BasicMessageProvider("en-US", [
|
|
135
|
+
["string.min", ({ constraint }) => `Minimum ${ constraint.formatted } character${ s(constraint.plural) }`],
|
|
136
|
+
["string.max", ({ constraint }) => `Maximum ${ constraint.formatted } character${ s(constraint.plural) }`],
|
|
137
|
+
["string.match", () => "Invalid format"],
|
|
138
|
+
|
|
139
|
+
["email.min", ({ constraint }) => `Minimum ${ constraint.formatted } character${ s(constraint.plural) }`],
|
|
140
|
+
["email.max", ({ constraint }) => `Maximum ${ constraint.formatted } character${ s(constraint.plural) }`],
|
|
141
|
+
["email.match", () => "Invalid email format"],
|
|
142
|
+
|
|
143
|
+
["time.min", ({ constraint }) => `Must be after or equal to ${ constraint.formatted }`],
|
|
144
|
+
["time.max", ({ constraint }) => `Must be before or equal to ${ constraint.formatted }`],
|
|
145
|
+
["time.match", () => "Invalid time format"],
|
|
146
|
+
|
|
147
|
+
["number.min", ({ constraint }) => `Must be greater or equal to ${ constraint.formatted }`],
|
|
148
|
+
["number.max", ({ constraint }) => `Must be less or equal to ${ constraint.formatted }`],
|
|
149
|
+
|
|
150
|
+
["date.min", ({ constraint }) => `Date must be greater or equal to ${ constraint.formatted }`],
|
|
151
|
+
["date.max", ({ constraint }) => `Date must be less or equal to ${ constraint.formatted }`],
|
|
152
|
+
|
|
153
|
+
["file.min", ({ constraint }) => `File must have a size of at least ${ constraint.formatted } byte${ s(constraint.plural) }`],
|
|
154
|
+
["file.max", ({ constraint }) => `File must have a size of at most ${ constraint.formatted } byte${ s(constraint.plural) }`],
|
|
155
|
+
|
|
156
|
+
["array.min", ({ constraint }) => `At least ${ constraint.formatted } element${ s(constraint.plural) }`],
|
|
157
|
+
["array.max", ({ constraint }) => `At most ${ constraint.formatted } element${ s(constraint.plural) }`],
|
|
158
|
+
|
|
159
|
+
["type", ({ constraint }) => `Wrong value type (expected ${ constraint.raw })`],
|
|
160
|
+
["test", ({ level }) => level === "pending" ? "Pending..." : level === "error" ? "Invalid value" : ""],
|
|
161
|
+
["oneOf", ({ constraint }) => `Must be one of: ${ constraint.formatted }`],
|
|
162
|
+
["exists", () => "Required field"],
|
|
163
|
+
["defined", () => "Required field"],
|
|
164
|
+
["notnull", () => "Required field"],
|
|
165
|
+
["required", () => "Required field"]
|
|
166
|
+
])
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* French (FR) message provider for validation messages.
|
|
170
|
+
* @category Localization
|
|
171
|
+
*/
|
|
172
|
+
export const messageProvider_fr_FR = new BasicMessageProvider("fr-FR", [
|
|
173
|
+
["string.min", ({ constraint }) => `Minimum ${ constraint.formatted } caractère${ s(constraint.plural) }`],
|
|
174
|
+
["string.max", ({ constraint }) => `Maximum ${ constraint.formatted } caractère${ s(constraint.plural) }`],
|
|
175
|
+
["string.match", () => "Format incorrect"],
|
|
176
|
+
|
|
177
|
+
["email.min", ({ constraint }) => `Minimum ${ constraint.formatted } caractère${ s(constraint.plural) }`],
|
|
178
|
+
["email.max", ({ constraint }) => `Maximum ${ constraint.formatted } caractère${ s(constraint.plural) }`],
|
|
179
|
+
["email.match", () => "Format d'email incorrect"],
|
|
180
|
+
|
|
181
|
+
["time.min", ({ constraint }) => `Doit être antérieur ou égal à ${ constraint.formatted }`],
|
|
182
|
+
["time.max", ({ constraint }) => `Doit être postérieur ou égal à ${ constraint.formatted }`],
|
|
183
|
+
["time.match", () => "Format horaire incorrect"],
|
|
184
|
+
|
|
185
|
+
["number.min", ({ constraint }) => `Doit être supérieur ou égal à ${ constraint.formatted }`],
|
|
186
|
+
["number.max", ({ constraint }) => `Doit être inférieur ou égal à ${ constraint.formatted }`],
|
|
187
|
+
|
|
188
|
+
["date.min", ({ constraint }) => `La date doit être postérieure ou égale au ${ constraint.formatted }`],
|
|
189
|
+
["date.max", ({ constraint }) => `La date doit être antérieure ou égale au ${ constraint.formatted }`],
|
|
190
|
+
|
|
191
|
+
["file.min", ({ constraint }) => `Le fichier doit avoir une taille d'au moins ${ constraint.formatted } octet${ s(constraint.plural) }`],
|
|
192
|
+
["file.max", ({ constraint }) => `Le fichier doit avoir une taille d'au plus ${ constraint.formatted } octet${ s(constraint.plural) }`],
|
|
193
|
+
|
|
194
|
+
["array.min", ({ constraint }) => `Au moins ${ constraint.formatted } élément${ s(constraint.plural) }`],
|
|
195
|
+
["array.max", ({ constraint }) => `Au plus ${ constraint.formatted } élément${ s(constraint.plural) }`],
|
|
196
|
+
|
|
197
|
+
["type", ({ constraint }) => `Valeur du mauvais type (${ constraint.raw } attendu)`],
|
|
198
|
+
["test", ({ level }) => level === "pending" ? "En cours..." : level === "error" ? "Valeur incorrecte" : ""],
|
|
199
|
+
["oneOf", ({ constraint }) => `Doit être parmi : ${ constraint.formatted }`],
|
|
200
|
+
["exists", () => "Champ obligatoire"],
|
|
201
|
+
["defined", () => "Champ obligatoire"],
|
|
202
|
+
["notnull", () => "Champ obligatoire"],
|
|
203
|
+
["required", () => "Champ obligatoire"]
|
|
204
|
+
])
|