diff --git a/README.md b/README.md index 01006af..216f018 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,13 @@ Many modern applications need to be internationalized, and this can be an issue In modern JavaScript development bundle size is always a concern - currently form-and-function weights in at just 3.54KB gzipped (14.87KB uncompressed). +## Contents + * [Examples](#examples) * [Installation](#installation) * [Usage](#usage) + * [Form](#form) + * [Field](#field) * [Validation](#validation) * [Built-In Validators](#built-in-validators) * [Custom Errors](#custom-validation-errors) @@ -152,11 +156,65 @@ export const YourApp = () => ( ); ``` +### Form + +The `Form` component will accept the following props: + +* name (string, required) - The name of the form. +* render (component/function, required) - Function/functional component that will render the form - passed InjectedFormProps as below +* renderProps (object, optional) - Custom props to pass to the render component +* validators (object, optional) - Form validation object - see validation +* initialValues (object, optional) - Initial form values in the form `{ [fieldName]: value }` +* onSubmit (function, optional) - Called on form submission with form values +* onSubmitFailed (function, optional) - Called when submission fails due to validation errors, with form values +* onChange (function, optional) - Called when any form value changes, with all form values + +The render component you provide will receive the following props: + +* Field (Component) - A component to create fields +* form (object) - Props that must be passed to a
element +* values (object) - Current form values +* meta (object) + * valid (boolean) - Is validation currently passing + * submitted (boolean) - Has the form been submitted at any time + * errors: (object) - Current errors for the form, { [fieldName]: { error: string }} + isValidating (boolean) - Is validation currently ongoing +* actions (object) + * reset (function) - Call to reset the form to initial values and clear validation errors + * submit: (function) - Call to submit the form +* ownProps (object) - Any additional props passed via `renderProps` above + +### Field + +The `Field` component (as provided to the `Form` renderer), can be passed the following props: + +* name (string, required) - The field name +* render (component/function, required) - Field renderer - passed InjectedFieldProps as below +* renderProps (object, optional) - Custom props to pass to the field renderer +* onChange (function, optional) - Called with the change event, and the field value, whenever the field value changes I.e. (e: SyntheticEvent, value: string | number | undefined) => void +* onFocus (function, optional) - Called with the focus event, and the field value, whenever the field value is focused +* onBlur (function, optional) - Called with the blur event, and the field value, whenever the field value is blurred + +The render component passed to `Field` is provided with the following props. The input prop should generally be passed directly to the underlying element, i.e. + +* meta (object) + * valid (boolean) - Does the field pass validation + * error (string | undefined) - Current validation error + * pristine (boolean) - True if the field has the same value as its initial value + * touched (boolean) - Has the field has ever been focused + * active (boolean) - Is the field currently focused + * isValidating (boolean) - Is the field currently being validated +* input (object) + * onChange (function) - Called with (event, value) when the field value changes + * onFocus (function) - Called with (event, value) when the field is focused + * onBlur (function) - Called with (event, value) when the field is blurred + * value (string | number | undefined) - Current field value + * name (string) - Name of the field +* ownProps - Any custom props passed to `Field`s `renderProps` + ### Validation -Validation follows a single route - a validators function is passed to the `Form` component, which should return entries -for any field that requires validation. This function is called with two "reporters" - `valid` and `invalid` - which -should be called by each individual field's validator depending on the validity of the field. `valid` takes +Validation follows a single route - a validators function is passed to the `Form` component, which should return an object with keys for any field that requires validation. This function is called with two "reporters" - `valid` and `invalid` - which should be called by each individual field's validator depending on the validity of the field. `valid` takes no arguments, and `invalid` should be called with a string describing the validation error. A convenience function `validators.create` is provided, which will pass `valid` and `invalid` to each entry in @@ -220,7 +278,7 @@ They can be used as follows: /> ``` -#### matches +#### equalTo Ensure that `passwordConfirm` field is the same as `password` field. @@ -292,9 +350,9 @@ value and the validator params, e.g. for `validation.atLeast`: #### Combining Validators -The inbuilt validators can be combined to validate on multiple conditions. As an example, we might want to -check if a field is numeric AND more than 5 characters. This is achieved with `validation.all`, which we can pass -an array of validators. +The inbuilt validators can be combined to validate on multiple conditions. `validation.all` ensures that all of an array of validators pass, `validation.any` ensures that at least one of a collection of validators pass. + +As an example, we might want to check if a field is numeric AND more than 5 characters. ```js value => { valid: true } | { valid: false; error: string } +interface ValidResult { + valid: true; +} + +interface InvalidResult { + valid: false; + error: string; +} + +({ valid, invalid }) => value => ValidResult | InvalidResult | Promise ``` -`valid` and `invalid` are functions that will generate appropriate results - if the value is valid then return `valid()`, if the value is invalid return `invalid("reason for invalidity")`. +`valid` and `invalid` are functions that will generate appropriate results - if the value is valid then return `valid()`, if the value is invalid return `invalid("reason for invalidity")`. Asynchronous functions are fine, just return a Promise that will resolve to a validation result. It's much easier to look at some example code, so let's make a validator that verifies that the provided value is an odd number of characters: @@ -417,7 +484,7 @@ be translated. The validators above are all usable with an i18n library, such as These examples assume that your application has been set up with react-intl, and you are somewhat familiar with its concepts. -The `validation.create` function above has an optional second argument - a formatter. This formatter has the signature: +The `validation.create` function above has an optional second argument - an `options` object, which includes a formatter. This formatter has the signature: `type Formatter = (x: T, params?: Record) => string;` @@ -465,7 +532,9 @@ const messages = { nonNumeric: messages.nonNumeric }) }, - this.props.intl.formatMessage + { + formatter: this.props.intl.formatMessage + } )} />; ``` @@ -479,7 +548,9 @@ If, as above, we name our messages using the same keys as the form validation me firstName: validation.atLeast({ chars: 3 }, messages), age: validation.numeric(messages) }, - this.props.intl.formatMessage + { + formatter: this.props.intl.formatMessage + } )} /> ``` diff --git a/src/demo/App.tsx b/src/demo/App.tsx index 4827d85..72f49a8 100644 --- a/src/demo/App.tsx +++ b/src/demo/App.tsx @@ -9,6 +9,8 @@ import { BasicForm } from "./forms/Basic"; import { ValidatedForm } from "./forms/Validated"; import { AsyncForm } from "./forms/Async"; import { IntlForm } from "./forms/Intl"; +import { RadioForm } from "./forms/RadioButtons"; +import { LinkedInputForm } from "./forms/LinkedInput"; addLocaleData([...en, ...fr]); @@ -30,6 +32,10 @@ export class App extends React.Component<{}, { locale: string }> { + + + + @@ -41,6 +47,10 @@ export class App extends React.Component<{}, { locale: string }> { + + + + ); diff --git a/src/demo/PrettyForm.tsx b/src/demo/PrettyForm.tsx index a421b30..5b8eb05 100644 --- a/src/demo/PrettyForm.tsx +++ b/src/demo/PrettyForm.tsx @@ -1,12 +1,15 @@ import * as React from "react"; -import { InjectedFormProps } from "../lib"; +import { InjectedFormProps, InjectedFieldProps } from "../lib"; import { Form, Button, Message, Header } from "semantic-ui-react"; import { PrettyField, PrettyFieldProps } from "./PrettyField"; +import { ReversingFieldProps, ReversingField } from "./ReversingField"; export interface PrettyField { name: string; label: string; hint: string; + key?: string; + reverse?: boolean; } export interface PrettyFormProps { @@ -17,7 +20,7 @@ export interface PrettyFormProps { } export const PrettyForm: React.SFC< - InjectedFormProps + InjectedFormProps > = ({ form, meta: { valid, errors, submitted }, @@ -27,15 +30,16 @@ export const PrettyForm: React.SFC< }) => (
{title}
- {fields.map(({ name, label, hint }) => ( + {fields.map(({ name, label, hint, key, reverse }) => ( ))} diff --git a/src/demo/PrettytRadio.tsx b/src/demo/PrettytRadio.tsx new file mode 100644 index 0000000..dd3b499 --- /dev/null +++ b/src/demo/PrettytRadio.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { InjectedFieldProps } from "../lib/index"; +import { Form, Radio } from "semantic-ui-react"; + +export const PrettyRadio: React.SFC< + InjectedFieldProps<{ value: string; title: string }> +> = ({ input, ownProps: { value, title } }) => ( + + + +); diff --git a/src/demo/ReversingField.tsx b/src/demo/ReversingField.tsx new file mode 100644 index 0000000..d6dc6e8 --- /dev/null +++ b/src/demo/ReversingField.tsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import { Form, Input, Label, Icon } from "semantic-ui-react"; +import { InjectedFieldProps } from "../lib"; + +export interface ReversingFieldProps { + submitted: boolean; + label: string; + hint: string; + reverse: boolean; +} + +const reverseString = (v?: string) => + ((v as string) || "") + .split("") + .reverse() + .join(""); + +export const ReversingField: React.SFC< + InjectedFieldProps +> = ({ + input, + meta: { valid, pristine, touched, active, error, isValidating }, + ownProps: { submitted, label, hint, reverse } +}) => ( + + + input.onChange(e, { + value: reverse + ? reverseString(e.currentTarget.value) + : e.currentTarget.value + }) + } + focus={active} + error={!valid} + icon={ + isValidating ? ( + + ) : ( + (touched || active || submitted) && + (valid ? ( + + ) : ( + + )) + ) + } + label={label} + /> + {!valid && active && error && } + +); diff --git a/src/demo/forms/Intl.tsx b/src/demo/forms/Intl.tsx index 7889e55..9c0001f 100644 --- a/src/demo/forms/Intl.tsx +++ b/src/demo/forms/Intl.tsx @@ -28,7 +28,7 @@ const makeValidators = ( { field1: validation.all( [ - // validation.atLeast({ chars: 7 }, messages), + validation.atLeast({ chars: 7 }, messages), validation.atMost({ chars: 2 }, messages) ], errors => @@ -37,7 +37,9 @@ const makeValidators = ( ), field2: validation.numeric(messages) }, - formatter + { + formatter + } ); export interface BasicFormNoIntlProps { diff --git a/src/demo/forms/LinkedInput.tsx b/src/demo/forms/LinkedInput.tsx new file mode 100644 index 0000000..c96a17e --- /dev/null +++ b/src/demo/forms/LinkedInput.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import { Form } from "../../lib"; + +import { PrettyForm } from "../PrettyForm"; +import { ReversingField } from "../ReversingField"; + +const fields = [ + { + name: "field1", + label: "Forward", + reverse: false, + key: "field1forward" + }, + { + name: "field1", + label: "Reverse", + reverse: true, + key: "field1backward" + } +]; + +export const LinkedInputForm = () => ( + { + console.log("Submitted with values", values); + }} + render={PrettyForm} + renderProps={{ + fields, + title: "Linked Form" + }} + /> +); diff --git a/src/demo/forms/RadioButtons.tsx b/src/demo/forms/RadioButtons.tsx new file mode 100644 index 0000000..524db09 --- /dev/null +++ b/src/demo/forms/RadioButtons.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { Form, InjectedFormProps, InjectedFieldProps } from "../../lib"; +import { Form as SURForm, Header, Radio } from "semantic-ui-react"; +import { PrettyRadio } from "../PrettytRadio"; + +export const RenderRadioForm: React.SFC< + InjectedFormProps<{ title: string }, { value: string; title: string }> +> = ({ + form, + meta: { valid, errors, submitted }, + actions: { reset }, + ownProps: { title }, + Field +}) => ( + +
{title}
+ + + +
+); + +export const RadioForm = () => ( + { + console.log("Submitted with values", values); + }} + render={RenderRadioForm} + renderProps={{ + title: "Radio Button Form" + }} + /> +); diff --git a/src/lib/Field.tsx b/src/lib/Field.tsx index 245a7d8..5fdf6a4 100644 --- a/src/lib/Field.tsx +++ b/src/lib/Field.tsx @@ -1,16 +1,16 @@ import * as React from "react"; -import { EventHandler, SyntheticEvent } from "react"; import { FormState } from "./Form"; import { StateEngine } from "./stateEngine"; import { FieldResult, isInvalidResult } from "./validation/index"; import { FieldValue } from "./Form"; +import { SyntheticEvent } from "react"; /** * These props are provided to the component provided as renderer * to the Field component. T is the type of the render components * own props, that will be provided via Fields renderProps prop */ -export interface InjectedFieldProps { +export interface InjectedFieldProps { meta: { valid: boolean; error?: string; @@ -20,10 +20,11 @@ export interface InjectedFieldProps { isValidating: boolean; }; input: { - onChange: EventHandler>; - onFocus: EventHandler>; - onBlur: EventHandler>; + onChange: FieldEventHandler; + onFocus: FieldEventHandler; + onBlur: FieldEventHandler; value: FieldValue | undefined; + name: string; }; ownProps: T; } @@ -38,6 +39,15 @@ export interface FieldMeta { isValidating: boolean; } +export interface SetValue { + value?: FieldValue; +} + +export type FieldEventHandler = ( + e: SyntheticEvent, + setValue?: SetValue +) => void; + /** * Props passed to the injected Field component when it is used */ @@ -45,9 +55,9 @@ export interface FieldProps { name: string; renderProps?: T; render: React.SFC>; - onChange?: EventHandler>; - onFocus?: EventHandler>; - onBlur?: EventHandler>; + onChange?: FieldEventHandler; + onFocus?: FieldEventHandler; + onBlur?: FieldEventHandler; } export interface FieldRecordAny> { @@ -88,21 +98,25 @@ export const makeField = ( */ constructor(props: FieldProps) { super(props); + + if (!resetState) { + return; + } + const initialValue = formActions.getInitialValue(props.name); // Setup initial state with validation as true - resetState && - this.updateState({ - meta: { - touched: false, - active: false, - validation: { - valid: true - }, - isValidating: false + this.updateState({ + meta: { + touched: false, + active: false, + validation: { + valid: true }, - value: initialValue - }); + isValidating: false + }, + value: initialValue + }); // Easy way to trigger validation of initialValue formActions.onChange(props.name, initialValue); @@ -184,14 +198,17 @@ export const makeField = ( * ensures that we immediately update the value even if validtion takes * some time. Returned promise is not currently used. */ - handleChange = (e: SyntheticEvent) => { + handleChange = (e: SyntheticEvent, setValue?: SetValue) => { // Pulled into a variable so we don't lose this when the event is disposed const { value } = e.currentTarget; const { onChange } = this.props; - onChange && onChange(e); + onChange && onChange(e, setValue); - formActions.onChange(this.props.name, value); + formActions.onChange( + this.props.name, + (setValue && setValue.value) || value + ); }; render() { @@ -227,7 +244,8 @@ export const makeField = ( onChange: this.handleChange, onFocus: this.handleFocus, onBlur: this.handleBlur, - value + value, + name }, meta: { valid, diff --git a/src/lib/Form.tsx b/src/lib/Form.tsx index 9c711c2..f51f277 100644 --- a/src/lib/Form.tsx +++ b/src/lib/Form.tsx @@ -13,13 +13,13 @@ import { InvalidFieldResult } from "./validation/index"; -export type FieldValue = string | boolean | number; +export type FieldValue = string | number; export type FieldValueMap = Record; export type FieldMap = Record; -export type EventHandler = (values: FieldMap) => void; -export type MaybeEventHandler = EventHandler | undefined; +export type FormEventHandler = (values: FieldMap) => void; +export type MaybeFormEventHandler = FormEventHandler | undefined; /** * Props supplied to the render component passed to Form @@ -52,14 +52,17 @@ export interface InjectedFormProps< /** * Props that can be passed to Form */ -export interface FormProps { +export interface FormProps< + T extends object | void = {}, + U extends object | void = {} +> { name: string; render: React.SFC>; validators?: Record>; initialValues?: FieldValueMap; - onSubmit?: EventHandler; - onSubmitFailed?: EventHandler; - onChange?: EventHandler; + onSubmit?: FormEventHandler; + onSubmitFailed?: FormEventHandler; + onChange?: FormEventHandler; renderProps?: T; stateEngine?: StateEngine; } @@ -97,17 +100,6 @@ export class Form< this.stateEngine = props.stateEngine || componentStateEngine(this, { - // fields: Object.entries(props.initialValues).reduce( - // (out, [field, value]) => ({ - // ...out, - // [field]: { - // value, - // meta: { - // validation: this.validate(field, value) - // } - // } - // }) - // ) as any, fields: {}, submitted: false, meta: { @@ -294,7 +286,7 @@ export class Form< } }); - (this.props.onChange as EventHandler)( + (this.props.onChange as FormEventHandler)( this.stateEngine.select(s => s.fields) ); }; @@ -304,16 +296,16 @@ export class Form< * depending on current validation status */ private handleSubmit = ( - onSubmit: MaybeEventHandler, - onFailedSubmit: MaybeEventHandler, + onSubmit: MaybeFormEventHandler, + onFailedSubmit: MaybeFormEventHandler, valid: boolean, fields: FieldMap ) => (e?: SyntheticEvent) => { e && e.preventDefault(); this.stateEngine.set({ submitted: true }); valid - ? (onSubmit as EventHandler)(fields) - : (onFailedSubmit as EventHandler)(fields); + ? (onSubmit as FormEventHandler)(fields) + : (onFailedSubmit as FormEventHandler)(fields); }; render() { diff --git a/src/lib/validation/all.ts b/src/lib/validation/all.ts index fce15d3..e1c0d8d 100644 --- a/src/lib/validation/all.ts +++ b/src/lib/validation/all.ts @@ -6,31 +6,31 @@ import { Reporters, ValidFieldResult, InvalidFieldResult, + CreateValidatorOptions, + ValidationResult, isValidResult, isInvalidResult } from "./typesAndGuards"; -import { FieldValue } from "../Form"; +import { FieldValue, FieldMap } from "../Form"; +import { FieldMeta, FieldRecordAny } from "../Field"; import { Formatter } from "./formatter"; /** * Combines validators so that all must be valid + * T is type of formatter result + * U is type of validation result * @param validators Validators to combine * @param combiner How to combine error messages */ -export const all = ( - validators: Array>, +export const all = ( + validators: CreateValidator[], combiner?: (errors: string[]) => string -) => ( - reporters: Reporters, - formatter: Formatter> -) => async (val: T): Promise => { +): CreateValidator => (reporters, options) => async ( + val, + fields +): Promise => { const results = await Promise.all( - validators.map( - validator => - validator(reporters, formatter)(val) as Promise< - FieldResult | CovalidatedFieldResult - > - ) + validators.map(validator => validator(reporters, options)(val, fields)) ); if (results.every(isValidResult)) { diff --git a/src/lib/validation/any.ts b/src/lib/validation/any.ts new file mode 100644 index 0000000..bea4f0e --- /dev/null +++ b/src/lib/validation/any.ts @@ -0,0 +1,39 @@ +import { + CreateValidator, + FieldResult, + CovalidatedFieldResult, + MessageParams, + Reporters, + ValidFieldResult, + InvalidFieldResult, + isValidResult, + isInvalidResult, + CreateValidatorOptions, + ValidationResult +} from "./typesAndGuards"; +import { FieldValue, FieldMap } from "../Form"; +import { FieldMeta, FieldRecordAny } from "../Field"; + +/** + * Combines validators so that at least one must be valid + * T is type of formatter result + * U is type of validation result + * @param validators Validators to combine + * @param combiner How to combine error messages + */ +export const any = ( + validators: CreateValidator[], + combiner?: (errors: string[]) => string +): CreateValidator => (reporters, options) => async (val, fields) => { + const results = await Promise.all( + validators.map(validator => validator(reporters, options)(val, fields)) + ); + + if (results.some(isValidResult)) { + return reporters.valid(); + } + + const errors = results.filter(isInvalidResult).map(r => r.error); + + return reporters.invalid(combiner ? combiner(errors) : errors.join(" or ")); +}; diff --git a/src/lib/validation/atLeast.ts b/src/lib/validation/atLeast.ts index bbe845d..4f35f90 100644 --- a/src/lib/validation/atLeast.ts +++ b/src/lib/validation/atLeast.ts @@ -4,7 +4,10 @@ import { Reporters, ValidFieldResult, InvalidFieldResult, - MessageParams + CovalidatedFieldResult, + MessageParams, + CreateValidator, + FieldResult } from "./typesAndGuards"; import { Formatter, useFormatter } from "./formatter"; @@ -18,24 +21,26 @@ export interface AtLeastParams { } /** - * Validates that a value is at least {chars} long + * Validates that a value is at least {chars} long Formatter> + * T is type of formatter result + * U is type of validation result * @param chars Minimum number of characters * @param msg Error messages when invalid */ -export const atLeast = ( +export const atLeast = ( params: AtLeastParams, - msg?: AtLeastMessages -) => ( - { valid, invalid }: Reporters, - formatter?: Formatter> -) => (value: string) => { - const format = useFormatter(msg, { ...params, value }, formatter); + msg?: AtLeastMessages +): CreateValidator => ( + { valid, invalid }, + options +) => value => { + const format = useFormatter(msg, { ...params, value }, options); if (!value) { return invalid(format("undef", `Please enter a value`)); } - return value.length >= params.chars + return value.toString().length >= params.chars ? valid() : invalid( format( diff --git a/src/lib/validation/atMost.ts b/src/lib/validation/atMost.ts index 916b076..f7f8a27 100644 --- a/src/lib/validation/atMost.ts +++ b/src/lib/validation/atMost.ts @@ -3,7 +3,10 @@ import { Reporters, MessageParams, ValidFieldResult, - InvalidFieldResult + InvalidFieldResult, + FieldResult, + CovalidatedFieldResult, + CreateValidator } from "./typesAndGuards"; import { Formatter, useFormatter } from "./formatter"; @@ -17,20 +20,25 @@ export interface AtMostParams { /** * Validates that a value is at most {chars} long + * T is type of formatter result + * U is type of validation result * @param chars Maximum number of characters * @param msg Error messages when invalid */ -export const atMost = (params: AtMostParams, msg?: AtMostMessages) => ( - { valid, invalid }: Reporters, - formatter?: Formatter> -) => (value: string) => { +export const atMost = ( + params: AtMostParams, + msg?: AtMostMessages +): CreateValidator => ( + { valid, invalid }, + formatter +) => value => { if (!value) { return valid(); } const format = useFormatter(msg, { ...params, value }, formatter); - return value.length <= params.chars + return value.toString().length <= params.chars ? valid() : invalid( format( diff --git a/src/lib/validation/covalidate.ts b/src/lib/validation/covalidate.ts index f842881..96f27fb 100644 --- a/src/lib/validation/covalidate.ts +++ b/src/lib/validation/covalidate.ts @@ -1,5 +1,6 @@ import { CreateValidator, + CreateValidatorOptions, FieldResult, Reporters, MessageParams, @@ -13,22 +14,27 @@ import { FieldMeta, FieldRecordAny } from "../Field"; /** * Specify that other fields should be revalidated when this field changes + * T is type of formatter result + * U is type of validation result * @param params Fields to covalidated * @param validator Validator for this field */ export const covalidate = ( params: { fields: string[] }, - validator: CreateValidator> -) => ( - reporters: Reporters, - formatter?: Formatter> -) => async (val: T, fields: FieldMap): Promise => { - console.log({ - covalidate: params.fields, - result: await validator(reporters, formatter)(val, fields) - }); + validator: CreateValidator< + FieldResult | Promise, + FieldValue, + T, + U + > +): CreateValidator< + CovalidatedFieldResult | Promise, + FieldValue, + T, + U +> => (reporters, options) => async (val, fields) => { return { covalidate: params.fields, - result: await validator(reporters, formatter)(val, fields) + result: await validator(reporters, options as any)(val, fields) }; }; diff --git a/src/lib/validation/create.ts b/src/lib/validation/create.ts index 8ebad43..3fb7370 100644 --- a/src/lib/validation/create.ts +++ b/src/lib/validation/create.ts @@ -1,9 +1,11 @@ import { + CreateValidatorOptions, CreateValidator, MessageParams, ValidFieldResult, InvalidFieldResult, - CovalidatedFieldResult + CovalidatedFieldResult, + Validator } from "./typesAndGuards"; import { Formatter } from "./formatter"; import { FieldValue } from "../Form"; @@ -29,16 +31,13 @@ export const invalidFn = (error: string): InvalidFieldResult => ({ * @param validationMap Object of field validators { [ fieldName: string ]: ValidationFunction } */ export const create = ( - validationMap: Record, - formatter?: Formatter> -) => { + validationMap: Record>, + options?: CreateValidatorOptions> +): Record> => { return Object.entries(validationMap).reduce( (out, [key, value]) => ({ ...out, - [key]: value( - { valid: validFn, invalid: invalidFn }, - formatter || (((x: string) => x) as any) - ) + [key]: value({ valid: validFn, invalid: invalidFn }, options) }), {} ); diff --git a/src/lib/validation/equalTo.ts b/src/lib/validation/equalTo.ts index f778e58..943ec3e 100644 --- a/src/lib/validation/equalTo.ts +++ b/src/lib/validation/equalTo.ts @@ -3,7 +3,10 @@ import { Reporters, MessageParams, ValidFieldResult, - InvalidFieldResult + InvalidFieldResult, + FieldResult, + CovalidatedFieldResult, + CreateValidator } from "./typesAndGuards"; import { Formatter, useFormatter } from "./formatter"; import { FieldMap } from "../Form"; @@ -18,18 +21,22 @@ export interface EqualToParams { * @param params The field to match * @param msg Error messages when invalid */ -export const equalTo = ( +export const equalTo = ( params: EqualToParams, msg?: MatchesMessages -) => ( - { valid, invalid }: Reporters, - formatter?: Formatter> -) => (value: string, fields: FieldMap) => { - const format = useFormatter(msg, { ...params, value }, formatter); +): CreateValidator => ( + { valid, invalid }, + options +) => (value, fields) => { + const format = useFormatter(msg, { ...params, value }, options); - if (value === fields[params.field].value) { - return valid(); - } else { + if ( + !fields || + !fields[params.field] || + value !== fields[params.field].value + ) { return invalid(format("different", `Must match ${params.field}`)); } + + return valid(); }; diff --git a/src/lib/validation/exactly.ts b/src/lib/validation/exactly.ts index 1a70307..ceb2c87 100644 --- a/src/lib/validation/exactly.ts +++ b/src/lib/validation/exactly.ts @@ -2,7 +2,10 @@ import { Message, Reporters, ValidFieldResult, - InvalidFieldResult + InvalidFieldResult, + FieldResult, + CovalidatedFieldResult, + CreateValidator } from "./typesAndGuards"; import { Formatter, useFormatter } from "./formatter"; import { FieldValue, FieldMap } from "../Form"; @@ -24,13 +27,13 @@ export interface ExactlyParams { export const exactly = ( params: ExactlyParams, msg?: ExactlyMessages -) => ( - { valid, invalid }: Reporters, - formatter?: Formatter> -) => (value: U, fields: FieldMap) => { - const format = useFormatter(msg, { ...params, value }, formatter); +): CreateValidator> => ( + { valid, invalid }, + options +) => (value, fields) => { + const format = useFormatter(msg, { ...params, value }, options); - if (value === params.value) { + if (((value as any) as U) === params.value) { return valid(); } else { return invalid(format("different", `Must be ${params.value}`)); diff --git a/src/lib/validation/formatter.ts b/src/lib/validation/formatter.ts index ff5132c..1249dfe 100644 --- a/src/lib/validation/formatter.ts +++ b/src/lib/validation/formatter.ts @@ -1,5 +1,10 @@ -import { Message } from "./typesAndGuards"; +import { Message, CreateValidatorOptions } from "./typesAndGuards"; +/** + * String formatter (react-intl compat) + * T is type of validator output + * U is type of validator params + */ export type Formatter = (x: T, params?: U) => string; /** @@ -13,7 +18,7 @@ export type Formatter = (x: T, params?: U) => string; export const useFormatter = ( msg: Partial>> | undefined, args: V, - formatter?: Formatter + options?: CreateValidatorOptions ) => (messageName: W, defaultMsg: string): string => { const message = msg && msg[messageName] @@ -21,5 +26,7 @@ export const useFormatter = ( ? (msg[messageName] as any)(args) : msg[messageName] : defaultMsg; - return formatter ? formatter(message, args) : message; + return options && options.formatter + ? options.formatter(message, args) + : message; }; diff --git a/src/lib/validation/index.ts b/src/lib/validation/index.ts index 7999cf9..13ea62e 100644 --- a/src/lib/validation/index.ts +++ b/src/lib/validation/index.ts @@ -1,4 +1,5 @@ export * from "./all"; +export * from "./any"; export * from "./atLeast"; export * from "./atMost"; export * from "./covalidate"; diff --git a/src/lib/validation/matches.ts b/src/lib/validation/matches.ts index d3e6764..62e1ffd 100644 --- a/src/lib/validation/matches.ts +++ b/src/lib/validation/matches.ts @@ -2,7 +2,10 @@ import { Message, Reporters, ValidFieldResult, - InvalidFieldResult + InvalidFieldResult, + FieldResult, + CovalidatedFieldResult, + CreateValidator } from "./typesAndGuards"; import { Formatter, useFormatter } from "./formatter"; @@ -19,17 +22,20 @@ export interface MatchesMessages { * @param params The RegExp against which to test the value * @param msg Error messages when invalid */ -export const matches = (params: MatchesParams, msg?: MatchesMessages) => ( - { valid, invalid }: Reporters, - formatter?: Formatter -) => (value: string) => { +export const matches = ( + params: MatchesParams, + msg?: MatchesMessages +): CreateValidator => ( + { valid, invalid }, + options +) => value => { if (!value) { return valid(); } - const format = useFormatter(msg, { ...params, value }, formatter); + const format = useFormatter(msg, { ...params, value }, options); - return params.regex.test(value) + return params.regex.test((value || "").toString()) ? valid() : invalid(format("different", `Must match pattern`)); }; diff --git a/src/lib/validation/numeric.ts b/src/lib/validation/numeric.ts index d0a4800..edf31bc 100644 --- a/src/lib/validation/numeric.ts +++ b/src/lib/validation/numeric.ts @@ -3,8 +3,11 @@ import { Message, Reporters, MessageParams, + CreateValidator, ValidFieldResult, - InvalidFieldResult + InvalidFieldResult, + FieldResult, + CovalidatedFieldResult } from "./typesAndGuards"; import { Formatter, useFormatter } from "./formatter"; @@ -17,17 +20,19 @@ export interface NumericMessages * Validates that a value is only numbers * @param msg Error messages when invalid */ -export const numeric = (msg?: NumericMessages) => ( - { valid, invalid }: Reporters, - formatter: Formatter> -) => (value: string) => { +export const numeric = ( + msg?: NumericMessages +): CreateValidator> => ( + { valid, invalid }, + options +) => value => { if (typeof value === "undefined") { return valid(); } - const format = useFormatter(msg, { value }, formatter); + const format = useFormatter(msg, { value }, options); - return /^[0-9]*$/.test(value || "") + return /^[0-9]*$/.test(value ? value.toString() : "") ? valid() : invalid(format("nonNumeric", `Entered value must be a number`)); }; diff --git a/src/lib/validation/required.ts b/src/lib/validation/required.ts index 1af708f..c27b83d 100644 --- a/src/lib/validation/required.ts +++ b/src/lib/validation/required.ts @@ -3,10 +3,13 @@ import { Reporters, MessageParams, ValidFieldResult, - InvalidFieldResult + InvalidFieldResult, + FieldResult, + CovalidatedFieldResult, + CreateValidator } from "./typesAndGuards"; import { Formatter, useFormatter } from "./formatter"; -import { FieldValue } from "../index"; +import { FieldValue } from "../Form"; export interface NotUndefinedMessage { undef?: Message; @@ -16,13 +19,13 @@ export interface NotUndefinedMessage { * Validates that a value is not undefined or zero-length * @param msg Error messages when invalid */ -export const required = (msg?: NotUndefinedMessage) => ( - { valid, invalid }: Reporters, - formatter: Formatter> -) => (value: FieldValue | undefined) => { - const format = useFormatter(msg, { value }, formatter); - - console.log({ valueRequired: value }); +export const required = ( + msg?: NotUndefinedMessage +): CreateValidator> => ( + { valid, invalid }, + options +) => value => { + const format = useFormatter(msg, { value }, options); return typeof value !== "undefined" && value.toString().length > 0 ? valid() diff --git a/src/lib/validation/typesAndGuards.ts b/src/lib/validation/typesAndGuards.ts index afe1cdf..4a9514a 100644 --- a/src/lib/validation/typesAndGuards.ts +++ b/src/lib/validation/typesAndGuards.ts @@ -51,22 +51,37 @@ export type Validator = ( fields?: FieldMap ) => U; +/** + * Options that can be passed to validator.create + * T is type of formatter input + * U is type of params + */ +export interface CreateValidatorOptions { + formatter?: Formatter; +} + /** * A function that takes reporters and optionally a formatter and applies them to a ValidatorFn - * T is type of field value - * U is type of formatter result - * W is type of validation result + * T is type of validation result + * U is type of field value + * V is type of formatter input + * W is type of validator params */ export type CreateValidator< - T = FieldValue | undefined, - U = any, + T = ValidationResult, + U = FieldValue | undefined, V = any, - W = ValidationResult -> = (reporters: Reporters, formatter?: Formatter) => Validator; + W = any +> = ( + reporters: Reporters, + options?: CreateValidatorOptions +) => Validator; /** * Takes params and applies them to a ValidationFnWithReporters * params will be more specifically typed by individual validators + * T is type of field value + * U is type of validation result */ export type CreateParameterizedValidator = ( ...params: any[] @@ -74,9 +89,11 @@ export type CreateParameterizedValidator = ( /** * Combined type of value and params passed to a formatter + * T is type of field value + * U is type of validator params */ -export type MessageParams = T & { - value: U; +export type MessageParams = U & { + value: T; }; /** @@ -85,7 +102,7 @@ export type MessageParams = T & { * U is value type * V is params type */ -export type Message = T | ((a: MessageParams) => T) | undefined; +export type Message = T | ((a: MessageParams) => T) | undefined; /** * Type guard for CovalidatedFieldResult diff --git a/tslint.json b/tslint.json index 1b608a1..32f6336 100644 --- a/tslint.json +++ b/tslint.json @@ -15,11 +15,7 @@ "jsx-no-multiline-js": false, "label-position": true, "max-line-length": [true, 120], - "member-ordering": [ - true, - "static-before-instance", - "variables-before-functions" - ], + "member-ordering": true, "no-arg": true, "no-bitwise": true, "no-consecutive-blank-lines": true, @@ -60,7 +56,6 @@ ], "variable-name": [ true, - "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"