rehance-forms
TypeScript icon, indicating that this package has built-in type declarations

4.5.1 • Public • Published

Rehance-Forms

This library aims to ease the pain of creating forms in React. It offers automatic state management and validation support, along with utility components and connected versions of standard HTML form control elements.

This library is written using Typescript and offers full typing support.

NOTE: Though inline fat arrow functions are used throughout this script, we recommend you hard-bind class methods instead.

Getting Started

Install via npm:

npm i -S rehance-forms

Usage

import * as React from "react";
import { Form, Input, ErrorOutput, SubmitButton } from "rehance-forms";
 
function LoginForm() {
  return (
    <Form onSubmit={(values) => /* your code here */}>
      <div>
        <Input
          type="email"
          name="email"
          required
        />
        <ErrorOutput name="email" />
      </div>
      <div>
        <Input
          type="password"
          name="password"
          required
          minLength={8}
          maxLength={26}
        />
        <ErrorOutput name="password" />
      </div>
      <SubmitButton>
        Login
      </SubmitButton>
    </Form>
  );
}

Standard Components

<Form>

The most critical component out of this list, the Form component sets up and provides the lightweight form API used by all other components. Unlike the other standard element components provided (Input, TextArea, Select) the Form component does not provide a 1-to-1 pass through for the standard <form> props (yet).

<Form onSubmit={values => console.log(values)} />

You can optionally supply an initialValues prop with an object containing the default values for all the fields of the form. This initial value is not only used to propogate the default values for each form field but also for determining what fields have changed from their initial value.

<Form initialValues={{ message: "Hello World" }}>
  <Input name="message" />
</Form>

<Input> and <TextArea>

The <Input> and <TextArea> components are direct abstractions over the standard <input> and <textarea> elements. They support nearly all of the standard props for their base elements, with a few exceptions. They automatically handle state management for input value and error states.

Checkboxes and Radios

Because this library manages the values of your fields from a top down approach, you cannot set the value for each field directly. For checkboxes and radios, this can be an issue since value is used to denote the value of the field when the element is considered "checked". You can get around this issue by padding the checkedValue prop.

// checkbox
<Input type="checkbox" name="exampleCheckbox" checkedValue="1" />
 
// radio
<Input type="radio" name="exampleRadio" checkedValue="blue" />

Validation Basics

Native browser validation is supported by default for these fields. This means you can simply set the required prop if a field is required or provide the type="email" prop if the <input> needs to be an email address.

<Input name="email" type="email" required />

These fields will automatically validate themselves based on any set validation rules when the component is first mounted. Subsequent validation occurs when the field is blurred by default. Alternatively, you can set the field to validate any time it is changed (in addition to when it's blurred) by providing the validateOnChange prop.

<Input name="example" required validateOnChange />

Custom Validation

You can override the default validation behaviour by providing your own validate function prop. The validation prop receives the current function name and access to all form values. If a string is returned, that string will be used as the error message for the field. If a falsey value is returned, then any existing error message for the field will be removed.

type Valdiator = (field: string, formValues: object) => string | null;

Value Formatting

By default, the value type passed to and from Input, TextArea and Select typically gets coerced to a string (since this is what the underlying HTML element does). However, you can choose to format this value to whatever you want using a format function prop. You could use this feature to create things such as form field masks.

type Formatter = (value: any, isOutput: boolean) => any;

Here's an example of how you might convert an object to a string input and vice versa. In this scenario, we want the data stored as an object {custom: ""} where the field is used to display and edit the custom key's value.

function formatToObject(value, output) {
  return output ? { custom: value } : value.custom;
}
 
<Input name="someObjectField" format={formatToObject} />;

Note: If you wish to edit many fields of an object within your form, it may be preferable to use a <Scope> instead. You can find more information about the <Scope> component below.

Customizations

The <Input> and <TextArea> components are simple wrappers around the default <input> and <texarea> elements and can be easily customized using CSS rules like input, input:focused and input:disabled. However, for greater control you can customize the CSS the controls using custom CSS classes via the className prop. You can either pass a string value, or if you need access to the field's state, you can supply a function.

// string value
<Input type="text" className="MyCustomInput" />
 
// custom function
<Input
  type="text"
  className={({ value, error, changed, touched }) => (
    "MyCustomInput" +
    (!!error ? " isInvalid" : "") +
    (touched ? " isDirty" : " isClean")
  )}
/>

<Select>

The <Select> component is a direct abstraction of the standard <select> and <option> elements. Rather than requiring the <option> children be provided manually, an options prop can be provided containing an array of string values or using { value: string, label: string } objects for each option to be rendered.

Validation handling for <Select> is identical to validation handling for <Input> and <TextArea>, as well as, adding custom formatting.

The following approaches work with the <Select> component.

// standard approach to <select>
<Select name="favColor">
  <option value="red">Red</option>
  <option value="green">Green</option>
  <option value="blue">Blue</option>
</Select>
 
// alternate approach 1
<Select
  name="favColor"
  options={["Red", "Green", "Blue"]}
/>
 
// alternate approach 2
<Select
  name="favColor"
  options={[
    { value: "red", label: "Red" },
    { value: "green", label: "Green" },
    { value: "blue", label: "Blue" },
  ]}
/>

Note: Providing an array of string values via the options prop will result in the <option> tags using the value for both the value and label of the option.

<Toggle>

The <Toggle> component is for creating custom boolean switches in place of a traditional <input type="checkbox" /> element. This component can be used in 2 different ways. Both approaches support the disabled prop, an onToggle event prop (for external change effects) and requires a name prop to be provided.

Customize Toggle With CSS

The first approach will create a single <span> element and apply the base className, along with, the appropriate classes for active and disabled states.

// the classes used here are the default classes provided by the component
<Toggle
  name="enableFeature"
  className="Toggle"
  activeClassName="isActive"
  disabledClassName="isDisabled"
  onToggle={enabled => alert(enabled)}
/>

Completely Custom Toggle Markup

Using a child render prop, custom markup can be supplied in the function and will be provided with the current value, the component disabled state and a toggle function for toggling the current value (when disabled is false).

<Toggle name="enableFeature">
  {({ value, disabled, toggle }) => (
    <span onClick={toggle}>{value ? "On" : "Off"}</span>
  )}
</Toggle>

<Radio>

The <Radio> component is extremely similar to the <Toggle> component and offers a quick way to create a customizable "radio" selection type of control. This component can be used in 2 different ways. Both approaches support the disabled prop, an onChange event prop (for external change effects) and requires name and value props to be provided.

Customize Radio With CSS

The first approach will create a single <span> element and apply the base className, along with, the appropriate classes for active and disabled states.

// the classes used here are the default classes provided by the component
<Radio
  name="favoriteColor"
  value="green"
  className="Radio"
  activeClassName="isActive"
  disabledClassName="isDisabled"
  onChange={state => alert(state)}
>
  Green
</Radio>

This approach also allows you to enable the ability for the radio option to be deselected on subsequent clicks after it's already active/selected. You can do this by supplying an allowDeselect prop. When the radio is deselected, it sets the value as null.

<Radio name="favoriteColor" value="green" allowDeselect>
  Green
</Radio>

Completely Custom Radio Markup

Using a child render prop, custom markup can be supplied in the function and will be provided with the component's value, the current groupValue, the component's disabled state and 2 field mutation methods: select and deselect. When using this approach, the allowSelect prop is not taken into account.

  • select() will set the radio group's value to the component's set value.
  • deselect() will set the radio group's value null.
<Radio name="favoriteColor" value="green">
  {({ groupValue, value, selected, disabled, select, deselect }) => (
    <span onClick={selected ? deselect : select}>
      {value} {selected ? "(Selected)" : ""}
    </span>
  )}
</Radio>

<MultiSelect>

The <MultiSelect> component is useful for creating toggles and checkboxes whose values should be added, or removed, from an array. It operates in an extremely similar fashion to <Toggle> and <Radio>, but the value outputted will be an array with the selections. This component can be used in 2 different ways. Both approaches support the disabled prop, an onChange event prop (for external change effects) and requires name and value props to be provided.

Customize MultiSelect With CSS

The first approach will create a single <span> element and apply the base className, along with, the appropriate classes for active and disabled states.

// the classes used here are the default classes provided by the component
<MultiSelect
  name="colors"
  value="green"
  className="MultiSelect"
  activeClassName="isActive"
  disabledClassName="isDisabled"
  onChange={state => alert(state)}
>
  Green
</MultiSelect>

Completely Custom MultiSelect Markup

Using a child render prop, custom markup can be supplied in the function and will be provided with the component's value, the current groupValue, the component's disabled state and 2 field mutation methods: select and deselect. When using this approach, the allowSelect prop is not taken into account.

  • select() will set the radio group's value to the component's set value.
  • deselect() will set the radio group's value null.
<MultiSelect name="colors" value="green">
  {({ groupValue, value, selected, disabled, toggle }) => (
    <span onClick={toggle}>
      {value} {selected ? "(Selected)" : ""}
    </span>
  )}
</MultiSelect>

<Subscriber>

The <Subscriber> component provides a quick and easy way to subscribe to specific field and form changes. It simply needs field prop for describing what fields should trigger a re-render. This field prop can be string containing the field name, an array of strings containing multiple field names or a function that determines whether it should update by returning a boolean value.

During a render, it passes the current form API to its child render prop and displays the results.

// any form changes
<Subscriber>
  {scope => (
    <span>Form/Scope Values: {scope.value}</span>
  )}
</Subscriber>
 
// single field
<Subscriber field="example">
  {scope => (
    <span>{scope.get("example")}</span>
  )}
</Susbcriber>
 
// multiple fields
<Subscriber field={["firstName", "lastName"]}>
  {scope => (
    <span>
      {scope.get("firstName")} {scope.get("lastName")}
    </span>
  )}
</Susbcriber>
 
// custom field predicate
<Subscriber field={fieldName => fieldName === "example"}>
  {scope => (
    <span>{scope.get("example")}</span>
  )}
</Susbcriber>

<PreviewOutput>

The <PreviewOutput> component provides a quick and simple way to display a preview of a field's value that automatically updates when the field changes. The returned value is wrapped in a <span> tag and supports all of the standard HTML span props like style and className.

<PreviewOutput name="example" />

Additionally, if the stored value for the field isn't a string, you can format the value by providing a format prop. For example, let's say we had an array of strings stored as todos that we wanted to list as a string of comma-separated values:

<PreviewOutput name="todos" format={todos => todos.join("")} />

<ErrorOutput>

Similarly to <PreviewOutput>, the <ErrorOutput> component provides a quick and simple way to display a field's validation error if one exists. The returned value is wrapped in a <span> tag and supports all of the standard HTML span props like style and className.

<ErrorOutput name="example" />

One subtle, but important detail to note, is that the initial error of a field is not shown by default. This is because the field has not been "touched" and displaying errors for a field that the user has not yet interacted with could be confusing or annoying. If you need the error message to show 100% of the time (so long as an error exists), you can provide the alwaysShow prop.

<ErrorOutput name="example" alwaysShow />

<Button>

The <Button> component provides a convenient way to create a <button> element that can be disabled based on your form's state. For example, if you want the button to be disabled if any errors exist on the form, you can provide the disabledOnError prop. Or if you want to disable the button when no changes have been made to the form, then you can provide the disabledUntilChanged prop.

<Button
  onClick={() => /* your code here */}
  disabledOnError
  disabledUntilChanged
>
  My Custom Action
</Button>

If you have a custom need, you can provide a disabled prop that accepts a standard boolean value or a predicate function that is given access to the form API instance for your form.

<Button
  onClick={() => /* your code here */}
  disabled={scope => /* custom logic here */}
>
  My Custom Action
</Button>

It's likely that you'll need your button to interact with the form/scope instance directly when clicked. To gain access to this convenience, you can supply a function to a onClickWithScope prop. The function provided will receive 2 arguments: the first is the mouse event and the second is the form/scope API.

<Button
  onClickWithScope={(ev, scope) => /* your code here */}
>
  My Custom Action
</Button>

<SubmitButton>

The <SubmitButton> component is a wrapper over top of the previously mentioned <Button> component. It automatically sets the disabledOnError and disabledUntilChanged props to true by default and submits the form when clicked. You can still provide a custom disabled prop or override the disabledOnError or disabledUntilChanged props, if desired.

<SubmitButton>Submit</SubmitButton>

<ResetButton>

The <ResetButton> component is a wrapper over top of the previously mentioned <Button> component. It automatically sets the disabledUntilChanged prop to true and resets the form values to their initialValues setting when clicked.

<ResetButton>Reset</ResetButton>

<ClearButton>

The <ClearButton> component is a wrapper over top of the previously mentioned <Button> component. It completely clears all fields when clicked and automatically disables itself when all form fields are empty.

<ClearButton>Clear Fields</ClearButton>

Scope & State Management

Under the hood, this library is using React's new Context API as a delivery mechanism for a lightweight API that manages the state information for the fields of your form. State changes are not broadcast through the React context as this can quickly cause expensive re-renders on many components at once. Instead, the lightweight API provides access to its own event bus for macro (form/scope) and micro (field) updates, allowing for components to be selective in what they respond to.

The underlying form API is referred to as the form scope, and it's not limited to a single level. The form scope can further be broken into child scopes, creating a scope tree for your form.

<FormScopeProvider> and <FormScopeConsumer>

The 2 React context components for providing and accessing a form API. If you'd like to know more about the props used by these components you can read the official React context documentation on the subject.

<Scope>

The <Scope> component allows you to create a new scope as a child of its parent scope (provided by either <Form> or another <Scope>). Nested field components will use the new scope provided by this component in place of its parent, allowing for isolation and encapsulation of scope values at this level.

<Form initialValues={{ foo: { bar: "" } }}>
  <Scope name="foo">
    <Input name="bar" required />
    <ErrorOutput name="bar" />
  </Scope>
</Form>

<CollectionScope>

The <CollectionScope> component is for creating a complex array of scopes for an array value within a form or scope. It takes a child render prop for rendering the scoped field elements for the dynamically created scopes. The render prop is provided with the numeric index of the item scope, the item's scope, and a remove function for removing the item (and its scope) entirely.

<Form initialValues={{ people: [] }}>
  <CollectionScope name="people">
    {({ index, total, scope, remove }) => (
      <div>
        <h4>Person #{index}</h4>
        <Input name="firstName" />
        <Input name="lastName" />
        <div>
          <span onClick={remove}>Remove Person</span>
        </div>
      </div>
    )}
  </CollectionScope>
</Form>

<AddCollectionItem>

The <AddCollectionItem> component creates a simple button for pushing new values onto an array field or <CollectionScope> within the current scope. It requires a to prop with the name of the target field or scope to push the value onto and can optionally provide a custom value prop. The default value added is an empty object literal.

<div>
  <CollectionScope name="people">{() => /* render fields */}</CollectionScope>
  <AddCollectionItem to="people" value={{ name: "" }}>
    Add Person
  </AddCollectionItem>
</div>

Higher-Order Components

withFormScope()

The withFormScope() function creates a higher-order component for quickly providing the wrapped component with the form API as a formScope prop. This is simply a utility over top of the <FormScopeConsumer> component.

const Example = withFormScope(function({ formScope }) {
  return <div />;
});

Package Sidebar

Install

npm i rehance-forms

Weekly Downloads

9

Version

4.5.1

License

MIT

Unpacked Size

205 kB

Total Files

32

Last publish

Collaborators

  • hello-helpfulhuman