@iwsio/forms
TypeScript icon, indicating that this package has built-in type declarations

4.0.0 • Public • Published

@iwsio/forms

@iwsio/forms: PUSH to main

See documentation for more examples and working demos.

This package combines browser form validation with React so you can more easily manage errors and input values in forms. More specifically, it tracks input validation in state making it available to React AND allows you to set input errors in state that in turn trigger setCustomValidity on DOM inputs enabling more control over how you render form errors in your applications.

Breaking Changes in 4.0

I've been continuing to make improvements while I use it myself in projects. This time, the breaking change is the signature on useFieldState(fields, defaults, onValidSubmit, errorMapping), which seems to be getting pretty long and ambiguious. The first two arguments are even the same type. So I've turned this into an object to more clearly define the options. This signature now looks like: useFieldState(fields, options) with options being nearly the same definition, just as an object: { defaults, errorMapping }.

I removed onValidSubmit from the fieldState altogether. This seemed oddly placed to me, and I started twisting myself trying dealing with chicken and egg problems: (like passing it into useFieldState while still using the output of this function within the onValidSubmit function). To clean this up, now onValidSubmit is solely a prop on the components: FieldManager and ControlledFieldManager.

Install

npm install @iwsio/forms
yarn install @iwsio/forms
pnpm install @iwsio/forms

Controlled/Uncontrolled inputs: <Input />, <Select />, and <TextArea />

These controlled inputs allow you to track error state with a controlled component value. They are identical to (and pass along) all the props from their counterparts input, select and textarea. These means you can use regular HTML 5 browser validation AND you can set custom errors easily with React state and let browser validation do the work of reporting or checking for invalid fields. These components include: Input, Select, and TextArea and work exactly how native elements work, the key difference being: you can include a fieldError and onFieldError props to manage error state.

<ValidatedForm>

Complimenting the inputs, I've included a form component to simplify styling and validated submit handling. It includes some CSS sugar needs-validation and was-validated for pre and post first submission. (This is kind of a throwback to Bootstrap, but you can use it however you like). You also still have acess to the psuedo classes :valid or :invalid as usual with these input components.

onValidSubmit invokes when form submit happens with all valid inputs. It's the same as using a regular <form/> onSubmit but with a built-in form.checkValidity() call to ensure field inputs are valid. className provided here will style the underlying HTML form.

Also, it should be mentioned that ValidatedForm works with controlled and uncontrolled inputs. It simply serves as a utility for the browser validation happening under the covers.

const Sample = () => {
  const handleValidSubmit = () => {
    // Form is valid; form inputs are safe to consume here.
  };

  return (
    <ValidatedForm onValidSubmit={handleValidSubmit}>
      <Input type="text" name="field1" required pattern="^\w+$" />
    </ValidatedForm>
  );
};

Renders:

Before submit

<form class="needs-validation">
  <input type="text" name="field1" required pattern="^\w+$" value="" />
</form>

After submit

<form class="was-validated">
  <input type="text" name="field1" required pattern="^\w+$" value="" />
</form>

useFieldState

Next there is useFieldState, which manages all the field values and error states for a form. This can be used independent of all the other components. The idea is to simply use a Record<string, string> type of state where the keys are the field names and they hold the current text value of the inputs. useFieldState manages both values and fieldErrors and provides methods to hook into that and make custom updates.

hook props Definition
checkFieldError helper function to check error state in combination with reportValidation; returns true when fieldError exists and reportValidation is true
fieldErrors current field errors; Record<string, FieldError> where keys match input names.
handleChange ChangeEventHandler<*> used to control the input.
onChange alias to handleChange
reportValidation boolean field that indicates if errors should be shown. Sets to true when using FieldManager after first form submit.
reset resets the fields back to defaultValues or initValues and resets fieldError state.
setField manually set a single field's value by name.
setFields manually set many values with a Record<string, string>. Undefined values are ignored.
setDefaultValues manually reset the default values.
setFieldError Sets a single field error. Useful when needing to set errors outside of browser validation.
setFieldErrors Sets ALL field errors. This is useful when you want to set more than one error at once. Like for example handling an HTTP 400 response with specific input errors.
setReportValidation sets reportValidation toggle for manual implementation.
isFormBusy Can be used to indicate the form has been submitted and is busy. This can be used to toggle disabled and styling state on DOM within the FieldManager.
toggleFormBusy Manages the isFormBusy state. During long-running async submit handlers, use this in combination with FieldManager prop: holdBusyAfterSubmit to hold the busy status after the initial onValidSubmit executes. Then clear the busy state with this method after the async process finishes.
const Sample = () => {
  const { fields, onChange, fieldErrors } = useFieldState({ field1: "" });

  return (
    <form>
      <input
        type="text"
        name="field1"
        required
        pattern="^\w+$"
        onChange={onChange}
        value={fields.field1}
      />
      {fieldErrors.field1 && <span>{fieldErrors.field1}</span>}
    </form>
  );
};

<FieldManager>

Finally, FieldManager brings it all together and automatically wires up field state and form validation. Rather than using the default Input, Select and TextArea components, you'll use extensions of those: InputField, SelectField, and TextAreaField respectively. The only prop (outside validation attributes) you need to provide is the name so FieldManager knows how to connect it to field state.

The props on FieldManager passthrough to the underlying <form/>. You can provide className and other HTML attributes commonly used on <form/>. For this to work however, you will need to provide at least an initial field state as fields. Use onValidSubmit to know when the form submit occurred with valid fields.

This can be manually styled as well. Use useFieldManager() to get a check function checkFieldError(fieldName: string). The result of this function will indicate whether there is an error and it is "reportable" (meaning the form has been submitted at least once). The real-time error is accessible using the fieldErrors state from the hook as well.

Here is a quick example:

const Sample = () => {
  const handleValidSubmit = (fields: FieldValues) => {
    // Do whatever you want with fields.
  }
  return (
    <FieldManager fields={{ field1: "", field2: "", field3: "" }} onValidSubmit={handleValidSubmit} className="custom-form-css" nativeValidation>
      <InputField type="text" name="field1" required pattern="^\w+$" />
      <InputField
        type="number"
        name="field2"
        required
        min={0}
        max={10}
        step={1}
      />
    
      <InputField type="phone" name="field3" required />
      <button type="submit">Submit</button>
    </FieldManager>
  )
}

<ControlledFieldManager>

You might be wondering, how do I manage field state outside of the FieldManager? Imagine a scenario where these field values may be managed upstream in your application. Maybe they're being pulled in from an asynchronous request and loaded after the form renders. In this case, you'll want to manage the field state outside of the FieldManager component. For this, there is another component you can use. <ControlledFieldManager/> where you provide the fieldState yourself. See this in action in the docs.

export const UpstreamChangesPage = () => {
	const { data, refetch, isFetching, isSuccess } = useQuery({ queryKey: ['/movies.json'], queryFn: () => fetchMovies() })
	const [success, setSuccess] = useState(false)

	const fieldState = useFieldState({ title: '', year: '', director: '' })
	const { setFields, reset, isFormBusy, toggleFormBusy } = fieldState

	const handleValidSubmit = useCallback((_values: FieldValues) => {
		setTimeout(() => { // NOTE: I added a quick half second delay to show how `isFormBusy` can be used.
			setSuccess(_old => true)
			toggleFormBusy(false)
		}, 500)
	}, [toggleFormBusy, setSuccess])

	useEffect(() => {
		if (!isSuccess || isFetching) return
		if (data == null || data.length === 0) return
		const { title, year, director } = data[0]
		setFields({ title, year, director })
	}, [isFetching, isSuccess])

	return (
		<ControlledFieldManager fieldState={fieldState} onValidSubmit={handleValidSubmit} holdBusyAfterSubmit className="flex flex-col gap-2 w-1/2" nativeValidation>
			<InputField placeholder="Title" name="title" type="text" className="input input-bordered" required />
			<InputField placeholder="Year" name="year" type="number" pattern="^\d+$" className="input input-bordered" required />
			<InputField placeholder="Director" name="director" type="text" className="input input-bordered" required />
			<div className="flex gap-2">
				<button type="reset" className="btn btn-info" onClick={() => refetch()}>Re-fetch</button>
				<button type="reset" className="btn" onClick={() => { reset(); setSuccess(false) }}>Reset</button>
				{/*
					NOTE: using holdBusyAfterSubmit on the FieldManager, which keeps the busy
					status true until manually clearing it. This allows us to use the `isFormBusy` flag
					to style the form and disable the submission button while an async process is busy.
				*/}
				<button type="submit" disabled={isFormBusy} className={`btn ${success ? 'btn-success' : 'btn-primary'} ${isFormBusy ? 'btn-disabled' : ''}`}>
					Submit
				</button>
			</div>
			<p>
				Try clicking <strong>Reset</strong> to reset the form. Then <strong>Submit</strong> will show validation errors. Then try clicking the <strong>Re-fetch</strong> button to fetch new data from the server and reset the field validation.
			</p>
		</ControlledFieldManager>
	)
}

<InvalidFeedbackForField />

One last component: this is just a helper component to display errors using the useFieldState properties mentioned above. Feel free to use this an example to make your own or consume it as-is. It currently returns a <span/> containing the error with any additional span attributes you provide as props. It consumes checkFieldError(name) to determine when to render and will return null when no error exists. You'll likely want to disable native validation reporting if you use this. Set nativeValidation={false} on the ValidatedForm or FieldManager, whichever one you use.

Source:

<InvalidFeedbackForField name="field1" className="text-[red] font-bold text-2xl" />

Renders with error:

<span className="text-[red] font-bold text-2xl">This field is required.</span>

Mapping Errors

When disabling browser validation and customizing styling around error reporting, you might want to change the text for each error. Well, you can by providing an errorMapping prop to the useFieldState hook or to the <FieldManager> component. You can see an example of customized error styling.

The basic idea is to create a ValidityState map assigning each type of error to a message.

// Note: These are intentionally vague for brevity.
const errorMapping: ErrorMapping = {
	badInput: 'Invalid',
	customError: 'Invalid',
	patternMismatch: 'Invalid',
	rangeOverflow: 'Too high',
	rangeUnderflow: 'Too low',
	stepMismatch: 'Invalid',
	tooLong: 'Too long',
	tooShort: 'Too short',
	typeMismatch: 'Invalid',
	valueMissing: 'Required'
}
const { setFieldError, checkFieldError } = useFieldState(fields, { errorMapping })

Or with FieldManager:

<FieldManager errorMapping={errorMapping} fields={fields}>

References:

MDN Forms validation

https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation

Package Sidebar

Install

npm i @iwsio/forms

Homepage

forms.iws.io

Weekly Downloads

4

Version

4.0.0

License

BSD-3-Clause

Unpacked Size

60.3 kB

Total Files

36

Last publish

Collaborators

  • iwsllc