npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@iwsio/forms

v4.3.3

Published

Simple library with useful React forms components and browser validation.

Downloads

287

Readme

@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