0.0.5-alpha1 • Published 8 years ago

yafl v0.0.5-alpha1

Weekly downloads
181
License
MIT
Repository
github
Last release
8 years ago

Yet. Another. Form. Library.

Build Status gzip size

YAFL - Yet Another Form Library

Motivation

Development on yafl only started after the release of React 16.3 and uses the React Context API behind the scenes to pass props between components. It has drawn a lot of inspiration from Redux-Form and Formik (both awesome projects!)

yafl's philosophy is to "keep it super simple". While it provides a lot of functionality out the box, it aims to keep it's API surface area as small as possible while still remaining flexible and easy to use.

Why use YAFL?

  • Use TypeScript to create strongly typed forms to give you peace of mind and a good nights sleep.
  • Create multi-page forms without needing to use specialized components or a state management library like flux or redux.
  • Create deeply nested forms or forms within forms.
  • Structure your Components to match the shape of your data. This means no more accessing field values using string paths!
  • Render a Validator!

Installation

npm i yafl

API

The Form Component

The Form component contains all the state that makes yafl work. All other yafl components have to be rendered as a child of a <Form>. Trying to render a Field outside of a Form, for example, will cause an error to be thrown.

Note: if you are nesting forms this may cause some pretty strange behaviour. If you have a use case for nested forms you'll have to use yafl's only non-component export: createFormContext.

Form Configuration Props

interface FormConfig<T extends object> {
  // The initial value of your Form. Once this value becomes
  // truthy your Form will initialize.
  initialValue?: T

  // The defaultValue is merged with initialValue on initialization
  // to replace any missing values.
  defaultValue?: T

  // When true, any time the initialValue prop changes your Form will not 
  // reinitialized with the updated initialValue. Default is false.
  disableReinitialize?: boolean

  // Specify whether values that are not matched with a rendered Field, Section or Repeat
  // should be included on submission of the form. Default is false.
  submitUnregisteredValues?: boolean

  // Specify whether your Form should remember what fields have been touched and/or
  // visited and if the submitCount should be reset to 0 when the initialValue prop changes.
  rememberStateOnReinitialize?: boolean

  // For convenience. Uses React's context API to make these values available to all
  // Field components.
  commonFieldProps?: { [key: string]: any }

  // For convenience. Allows you specify component dictionary to match a Fields component prop with.
  componentTypes?: ComponentTypes<T>

  // The function to call on form submission
  onSubmit?: (formValue: T) => void

  // Will get called every time a the Form state changes. Implemented inside the Form's 
  // componentDidUpdate function which means the same cautions apply when calling setState 
  // here as do in any component's componentDidUpdate function
  onStateChange?: (previousState: FormState<T>, nextState: FormState<T>) => void
}

The Field Component

Field components are the bread and butter of any form library and yafl's Field's are no exception! The <Field /> component is more or less equivalent to the Field components found in Formik or Redux-Form. The most important thing to note about the Field component is that you should never name your Field using a 'path' string. Yafl uses a Fields location in the Form's component hierarchy to determine the shape of the resulting form value.

interface FieldConfig<F extends object, T = any> {
  // Name your field! Providing a number usually indicates that
  // this field appears in an array.
  name: string | number
  
  // Transforms a Field's value before setting it. Useful for number inputs and the like.
  parse?: (value: any) => T
  
  // A render prop that accepts an object containing all the good stuff
  // you'll need to render a your Field.
  render?: (props: FieldProps<F, T>) => React.ReactNode
  
  // Specify a component to render. If a string is provided then yafl will attempt to 
  // match the string component to one provided in the componentTypes Form prop
  // and if no match is found then it will call React.createElement with the value provided.
  component?: React.ComponentType<FieldProps<F, T>> | string
  
  // Any other props will be forwarded (along with any props specified by
  // commonFieldProps on the Form component) to your component or render prop.
  [key: string]: any
}

Field Props

The following is a list of props that are passed to the render prop or component prop for every Field where T and F correspond to the generic types for the Field and Form respectively.

PropDescription
input: InputProps<T>An object containing the core handlers and props for an input. Allows for easy use of the spread operator.
path: stringThe path for this field.
visited: booleanIndicates whether this Field has been visited. Automatically set to true on when input.onBlur() is called.
touched: booleanIndicates whether this Field has been touched. Automatically set to true the first time a Field's value is changed.
isDirty: booleanIndicates whether the initialValue for this Field is different from its current value.
isActive: booleanIndicates whether this Field is currently in Focus.
isValid: booleanIndicates whether this Field is valid based on whether there are any Validators rendered that match the path of this Field.
errors: string[]An array containing any errors for this Field based on whether there are any Validators rendered that match the path of this Field.
initialValue: TThe value this Field was initialized with.
defaultValue: TThe default value that this Field was initialized with.
setValue: (value: T, touch?: boolean) => voidSets the value for this Field. Optionally specify if this Field should be touched when this function is called. Default is true.
formValue: FThe current value of the Form
submitCount: numberThe number of times the Form has been submitted.
resetForm: () => voidClears all Form state. formValue is reset to its initialValue.
submit: () => voidCalls the onSubmit function supplied to the Form component
forgetState: () => voidResets submitCount, touched and visited. The formValue is not reset.
clearForm: () => voidClears all Form state. formValue is reset to its defaultValue.
setFormValue: (set: SetFormValueFunc<F>) => voidSets the formValue imperatively.
setFormVisited: (set: SetFormVisitedFunc<F>) => voidSets the Form's visited state imperatively. Accepts a callback with the Form's previous value.
setFormTouched: (set: SetFormTouchedFunc<F>) => voidSets the Form's touched state imperatively. Accepts a callback with the Form's previous visited state.

Field InputProps

PropDescription
name: stringForwarded from the name prop of this Field.
value: TThe current value of this Field.
onBlur: (e: React.FocusEvent<any>) => voidThe onBlur handler for your input (DOM only). Useful if you need to keep track of which Fields have been visited.
onFocus: (e: React.FocusEvent<any>) => voidThe onFocus handler for your input (DOM only). Useful if you need to keep track of which field is currently active.
onChange: (e: React.ChangeEvent<any>) => voidThe onChange handler for your input (DOM only). Sets the value of this Field.

The Section Component

Section components give your forms depth. The name prop of a <Section /> will become the key of an object value in your Form. If a <Field /> appears anywhere in a sections children it will be a property of that section. So, for example, the following piece of JSX

// Leaving out some required props for the sake of brevity
<Form>
  <Field name="fullName" />
  <Section name="contact">
    <Field name="tel" />
    <Section name="address" fallback={{ streetNo: '', streetName: '', city: ''  }}>
      <Field name="streetNo" />
      <Field name="streetName" />
      <Field name="city" />
    </Section>
  </Section>
</Form>

will produce a formValue object that looks like

  {
    fullName: "",
    contact: {
      tel: "",
      address: {
        streetNo: "",
        streetName: "",
        city: ""
      }
    }
  }

Cool, huh!

Section Configuration Props

interface SectionConfig<T> {
  // Like a Field, a Section also requires a name prop!
  name: Name

  // The fallback prop is similar to the default value prop on the Form component,
  // except the difference is that it never gets merged with the formValue.
  // Useful if the value for the Section is ever null or undefined. A fallback becomes especially handy
  // if your Section component is rendered within a Repeat. Since it usually doesn't make much sense to assign
  // anything but an empty array[] as the default value for a list of objects, we can specify a fallback value
  // to prevent warnings about uncontrolled inputs become controlled inputs.
  fallback?: T

  children: React.ReactNode
}

The Repeat Component

The Repeat component is conceptually similar to the Section component except that it can be used to create what other libraries call "FieldArrays". A <Repeat /> uses a function as children and comes with a few handy helper methods. Here's an example using TypeScript

interface Movie {
  title: string
  releaseDate: Date | null
  rating: number
}

<Form>
  {/* using JSX generic type arguments which were introduced in TypeScript 2.9 */}
  <Repeat<Movie> name="movies" fallback={[]}>
    {(arr, { push, remove, insert }) => {
      return (
        <>
          {arr.map((item, i) => (
            <Section<Movie> name={i}>
              <Field<string> name="title" />
              <Field<string> name="releaseDate" />
              <Field<number> name="rating" />
              <button onClick={() => remove(i)}>Remove</button>
            </Section>
          ))}
          {/* yes, TypeScript will catch any type errors when calling push()!*/}
          <button onClick={() => push({ title: "", releaseDate: null, rating: 5 })}>Add</button>
        </>
      )
    }}
  </Repeat>
</Form>

Will produce...

  {
    movies: [
      {
        title: "",
        releaseDate: null,
        rating: 5
      },
      ...
    ]
  }

Repeat Configuration Props

interface RepeatConfig<T> {
  name: Name

  // Serves the same purpose as a Section's fallback prop. This is usually more useful when dealing with arrays
  // since is allows you to call value.map() without worrying about value null or undefined
  fallback?: T[]

  children: ((value: T[], utils: ArrayHelpers<T>) => React.ReactNode)
}

Repeat Props

The Repeat Component uses the function as a child pattern. The first argument is the value of this Repeat section. The second argument is an object of array helper functions which provide some simple array manipulation functionality.

children: (value: T[], utils: ArrayHelpers<T>) => React.ReactNode

Array Helpers

PropDescription
push: (...items: T[]) => numberAppends new elements to the array, and returns the new length of the array.
pop: () => T \| undefinedRemoves the last element from the array and returns it.
shift: () => T \| undefinedRemoves the first element from the array and returns it.
unshift: (...items: T[]) => numberInserts new elements at the start of the array and returns the new length of the array.
insert: (index: number, ...items: T[]) => numberInserts new elements into the array at the specified index and returns the new length of the array.
swap: (index1: number, index2: number) => voidSwaps two elements at the specified indices.
remove: (index: number) => T \| undefinedRemoves an element from the array at the specified index.

The Gizmo Component

Gizmos are general purpose components that can be used to render anything that isn't a field - a submit button is the obvious example, but this could be anything. Another possible use case for the <Gizmo /> component is to create your own higher order components! Since a Gizmo is a pure Consumer you can render Fields, Sections and Repeats within a Gizmo so it becomes simple to decorate any component of your choice with any or all the functions that you might need. Lets take a look:

// withForm.js
import { Gizmo, Form } from 'yafl'

export default (Comp) => ({ initialValue, onSubmit, /* other Form props */ children, ...props }) => (
  <Form
    onSubmit={onSubmit}
    initialValue={initialValue}
  >
    <Gizmo render={utils => <Comp {...utils} {...props}>{children}</Comp>} />
  </Form>
)
// SimpleForm.js
import withForm from './withForm'

const MyForm = (props) => (
  <React.Fragment>
    <Field name="email" render={({ input }) => <input {...input} />} />
    <Field name="password" render={({ input }) => <input {...input} />} />
    <button disabled={!props.formIsValid} onClick={props.submit}>Login</button>
  </React.Fragment>
)

export default withForm(MyForm)

Gizmo Configuration Props

interface GizmoConfig<F extends object> {
 render?: (props: GizmoProps<F>) => React.ReactNode

 component?: React.ComponentType<GizmoProps<F>>

// Any other props will be forwarded to your component
 [key: string]: any
}

The Validator Component

The <Validator /> component can be 'rendered' to create errors on your Form. The concept of "rendering a validator" might require a small shift in the way you think about form validation since other form libraries usually do validation through the use of a validate prop. With yafl however, you validate your form by simply rendering a Validator. This has some interesting benefits, one of which is that a "rendered" validator solves some of the edge cases around form validation - the most obvious example being that of async validation.

Here's an example:

// ValidatorExample.js
import { Form, Field, Validator } from 'yafl'

<Form>
  <Field
    name="email"
    label="Email" // unrecognized props are forwarded to your component
    component={TextInput}
  />
  <Field
    name="password"
    label="Password"
    minLength={6}
    component={TextInput}
  />
  <Field
    name="confirmPassword"
    label="Confirm Password"
    component={TextInput}
  />
  <Gizmo
    render={({ formValue }) => formValue.password !== formValue.confirmPassword && (
      <Validator path="issues" msg="Oops, passwords do not match!" />
    )}
  />
</Form>

function TextInput({ input, field, minLength, label }) {
  return (
    <Fragment>
      <label>{label}</label>
      <input type="text" {...input} />
      <IsRequired message="This field is required" {...field} />
      <Length message="Too short!" min={minLength} {...field}  />
    </Fragment>
  )
}

function IsRequired ({ value, touched, visited, validateOn = 'blur', message }) {
  return <Validator invalid={!value} msg={message} />
}

function Length ({ value, touched, visited, validateOn = 'change', min, max, message }) {
	return (
		<Validator 
			msg={message} 
			invalid={value.length < min) || (value.length > max}
		/>
	)
}

Nice and declaritive.

Validator Configuration Props

interface ValidatorProps {

  // Defaults to false. When the invalid prop becomes true the Validator will set a Form
  // Error for the corresponding path. If the invalid prop is not provided then an error will only
  // be set if and when the msg prop is passed a string value.
  invalid?: boolean

  // The error message. If this Validator component is rendered
  // with the same path as another Validator component
  // the msg string will the pushed onto an array of error messages for the same path.
  msg: string

  // Override the path for a Validator. By default the path is determined by what
  // appears above this component in the Form component hierarchy. Useful for errors
  // that belong in the domain of a Section, Repeat, at the Form level
  // or for general errors.
  path?: Path
}

Top Level API

yafl only exports a single function:

createFormContext returns all of the same components as those exported by yafl.

const { Form, Field, Section, Repeat, Gizmo, Validator } = createFormContext()

There are a few cases where one might want to nest one Form within another. However, since yafl uses React's context API to pass props from Provider to Consumer, rendering a Form inside another Form will make it impossible to access the outter Form values within a Field, Section or Repeat that are rendered within the inner Form. The following example serves to illustrate the problem:

import { Form, Field, Section } from 'yafl'

const ProblemForm = (props) => {
  return (
    <Form> // Call me Form A!
      <Section name="sectionA">
        <Field
          name="formAField1"
          render={(props) => {
            // I correctly belong to Form A
            return null
          }}
          />
        <Form>  // I am Form B!
          <Section name="sectionB">
            <Field
              name="formBField1"
              render={(props) => {
                // I correctly belong to Form B
                return null
              }}
            />
            <Field
              name="formAField2"
              render={(props) => {
                // Oops! I belong to Form B!
                return null
              }}
            />
          </Section>
        </Form>
      </Section>
    </Form>
  )
}

So how do we solve this?

import { Form as FormA, Field as FieldA, Section as SectionA, createFormContext } from 'yafl'

const context = createFormContext()

const FormB = context.Form
const FieldB = context.Field
const SectionB = context.Section

const NestedFormExample = (props) => {
  return (
    <FormA>
      <SectionA name="sectionA1">
        <FieldA name="formAField1" />
        <FormB>
          <SectionA name="sectionA2">
            <FieldA
              name="formAField2"
              render={props => {
                // Sweet! I belong to Form A even though I am rendered inside of Form B!
                return null
              }}
            />
            <FieldB
              name="formAField1"
              render={props => {
                // Sweet! I belong to Form B even though I am rendered inside of Section A!
                return null
            />
          </SectionA>
        </FormB>
      </SectionA>
    </FormA>
  )
}
1.1.2

6 years ago

1.1.1

6 years ago

1.1.0

6 years ago

1.0.1

6 years ago

1.0.0

6 years ago

1.0.0-rc.15

6 years ago

1.0.0-rc.14

6 years ago

1.0.0-rc.13

6 years ago

1.0.0-rc.12

6 years ago

1.0.0-rc.11

6 years ago

1.0.0-rc.9

6 years ago

1.0.0-rc.8

6 years ago

1.0.0-rc.6

6 years ago

1.0.0-rc.5

6 years ago

1.0.0-rc.4

6 years ago

1.0.0-rc.2

6 years ago

1.0.0-rc.1

7 years ago

1.0.0-hooks-6

7 years ago

1.0.0-hooks-5

7 years ago

1.0.0-hooks-4

7 years ago

1.0.0-hooks-3

7 years ago

1.0.0-hooks-2

7 years ago

1.0.0-hooks-1

7 years ago

1.0.0-ctx-6

7 years ago

1.0.0-ctx-5

7 years ago

1.0.0-ctx-4

7 years ago

1.0.0-ctx-3

7 years ago

1.0.0-ctx-2

7 years ago

1.0.0-ctx-1

7 years ago

1.0.0-alpha1

7 years ago

0.0.17-alpha1

7 years ago

0.0.16

7 years ago

0.0.15

7 years ago

0.0.14

7 years ago

0.0.14-alpha4

7 years ago

0.0.14-alpha3

7 years ago

0.0.14-alpha2

7 years ago

0.0.14-alpha1

7 years ago

0.0.13

7 years ago

0.0.13-alpha4

7 years ago

0.0.13-alpha3

7 years ago

0.0.13-alpha2

7 years ago

0.0.13-alpha1

7 years ago

0.0.12

8 years ago

0.0.12-alpha1

8 years ago

0.0.11

8 years ago

0.0.11-alpha2

8 years ago

0.0.11-alpha1

8 years ago

0.0.10

8 years ago

0.0.10-alpha2

8 years ago

0.0.10-alpha1

8 years ago

0.0.9

8 years ago

0.0.8

8 years ago

0.0.8-alpha1

8 years ago

0.0.7

8 years ago

0.0.7-alpha4

8 years ago

0.0.7-alpha3

8 years ago

0.0.7-alpha2

8 years ago

0.0.7-alpha1

8 years ago

0.0.6

8 years ago

0.0.5-alpha1

8 years ago

0.0.5

8 years ago

0.0.4

8 years ago

0.0.3

8 years ago

0.0.2

8 years ago

0.0.1-alpha3

8 years ago

0.0.1-alpha2

8 years ago

0.0.1-alpha1

8 years ago

0.0.1-alpha

8 years ago

0.0.1

8 years ago