Skip to content

React form library with very few opinions about your markup.

License

Notifications You must be signed in to change notification settings

react-noui/create-form

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

create-form

gzip size npm version PRs Welcome

create-form is a declarative, typescript-driven form control library. create-form allows you to quickly apply setters/getters/validators within atomic, props-less components by following React's Context API.

This README was generated by anansi.

Installation

yarn add @react-noui/create-form

Create form

This library employs the factory pattern when creating the data-layer for your form controls. If you are familiar with React.createContext then you will be familiar with how to use this library.

Example

createForm creates the state management for Controlled inputs. This example uses

type Todo = {
  userId: number;
  title: string;
  completed: boolean;
}
const TodoForm = createForm<Todo>({
  props: {
    completed: {
      type: 'checkbox',
    },
    title: {
      placeholder: 'What needs to be done?',
    },
    userId: {
      type: 'hidden', // required, but only for fetch
    },
  },
});
function CreateTodo({ user }: { user: User }) {
  return (
    <TodoForm.Provider defaultValues={{ userId: user.id, title: '', completed: false }}>
      <TodoCompleted />
      <TodoTitle />
      <TodoUserId />
      <CreateTodoSave />
    </TodoForm.Provider>
  );
}
function EditTodo({ user, todo }: { user: User, todo: Todo }) {
  return (
    <TodoForm.Provider defaultValues={todo}>
      <TodoCompleted />
      <TodoTitle />
      <TodoUserId />
      <EditTodoSave todoId={todo.id} />
    </TodoForm.Provider>
  );
}
function TodoCompleted() {
  const { completed } = useForm(TodoForm);
  return <input {...completed} />
}
function TodoTitle() {
  const { title } = useForm(TodoForm);
  return <input {...title} />
}
function TodoUserId() {
  const { userId } = useForm(TodoForm);
  return <input {...userId} />
}
function EditTodoSave({ todoId }: { todoId: number }) {
  const form = useForm(TodoForm);
  const handleClick = useCallback(() => {
    fetch(`https://jsonplaceholder.typicode.com/todos/${todoId}`, {
      method: 'PUT',
      body: JSON.stringify(form.toJSON()),
    });
  }, [form, todoId]);
  return <button onClick={handleClick}>Update</button>
}
function CreateTodoSave() {
  const form = useForm(TodoForm);
  const handleClick = useCallback(() => {
    fetch('https://jsonplaceholder.typicode.com/todos', {
      method: 'POST',
      body: JSON.stringify(form.toJSON()),
    });
  }, [form]);
  return <button onClick={handleClick}>Create</button>
}

Controlled

const MyForm = createForm<T>(options? = {})

import { createForm } from '@react-noui/create-form';

type Login = {
  email: string;
  password: string;
  session: boolean;
}

const LoginForm = createForm<Login>()
// -> { Context, Provider }

Options

Props

options.props: Record<K = keyof T, DetailedHTMLProps<HTMLInputElement>>

These props map directly to the desired field. Props can be any semantic HTMLInputElement propery.

const LoginForm = createForm<Login>({
  props: {
    email: {
      type: 'email',
      placeholder: 'Account email',
    },
    password: {
      type: 'password',
      placeholder: 'Account password',
    },
  },
});

Validation

options.validate: Record<K = keyof T, (value: T[K], values: T) => string | undefined>

Provide custom validation methods upon receiving changes to key K of provided type T. The keys allowed in your validate option must match the keys you provided from type T, but are not required.

For the LoginForm example, we can provide options to validate email | password:

const LoginForm = createForm<Login>({
  validate: {
    email: (value) => !value.length ? 'Email cannot be empty' : undefined,
    password: (value) => !value.length ? 'Password cannot be empty' : undefined,
  },
});

Uncontrolled

If you don't want to use controlled inputs, you can create an uncontrolled form. Most of the controlled APIs do not exist for uncontrolled forms due to the nature of uncontrolled inputs. Also, options.validate is not possible because onChange events are no longer run against the individual inputs.

const MyFormUncontrolled = createFormUncontrolled<T>(options? = {})

import { createFormUncontrolled } from '@react-noui/create-form';
const LoginForm = createForm<Login>({
  props: {...}
})
// -> { Context, Provider }

Hooks

Controlled

useForm(LoginForm)

Short-cut access to the LoginForm APIs. This should be composed within LoginForm.Provider.

function LoginFormConsumer() {
  const form = useForm(LoginForm)
  // ...
}
attribute type effect
form.reset(field) (form[FIELD]) => void Resets form[FIELD] values, errors, files, etc. Resets form[FIELD].currentto the defaultValues[field.name] for field.
form.resetAll() () => void Resets form[FIELD] values, errors, files, etc. Resets form[FIELD].currentto the defaultValues for all fields.
form.toJSON() () => void Returns JSON format matching shape T
form.toFormData() () => FormData Obtain FormData for use with http request/fetch Content-Type: application/x-www-form-urlencoded.
form.toURLSearchParams() () => string Returns query string with url-encoded form fields
form.options {props: {...}, validate: {...}} Returns the options used in createForm(options)

Uncontrolled

useFormUncontrolled(LoginForm)

Short-cut access to the Uncontrolled LoginForm APIs. This should be composed within LoginForm.Provider.

function LoginFormConsumer() {
  const form = useFormUncontrolled(LoginForm)
  // ...
}
attribute type effect
form.options {props: {...}} Returns the options used in createFormUncontrolled(options)

Provider LoginForm.Provider

This behaves like React.createContext(...).Provider such that access to LoginForm values can only be made from components within this provider.

const LOGIN_DEFAULTS: Login = {
  email: '',
  password: '',
  session: false,
}
function LoginFormPage() {
  return (
    <LoginForm.Provider defaultValues={LOGIN_DEFAULTS}>
      {...components}
    </LoginForm.Provider>
  )
}

Provider prop defaultValues: T

Where T extends Record<string, string | number | boolean>. This library currently supports primitive values. HTML change events typically involve primitive values.

This will be the default values we use in our form as type T. In the example above, T is of type Login.

LoginForm.Context

This behaves like React.createContext(...) such that access to the LoginForm APIs can be made.

// Controlled inputs
function LoginFormEmailComponent() {
  const form = React.useContext(LoginForm.Context);
  return (
    <>
      <label htmlFor={form.email.name}>Email</label>
      <input
        id={form.email.name}
        name={form.email.name}
        value={form.email.value}
        onChange={(event) => form.set.email(event.target.value)}
      />
      {form.email.error && <div>{form.email.error}</div>}
    </>
  )
};

const field = useContext(MyForm.Context)[FIELD]

Each field defined in type T is accessible via form[FIELD]

attribute type effect
[FIELD].default T[keyof T] Default value provided by you within <MyForm.Provider defaultValues={value} />.
[FIELD].current T Current state of the form value.
[FIELD].set(value: T[keyof T]) (value: T[keyof T]) => void Allows setting of [FIELD].current values for matching [FIELD].
[FIELD].error string | undefined Custom string will exist if you have the matching options.validate[FIELD](value: T[keyof T]) defined.
[FIELD].name string Provides a random, unique value for use as a primary key for any given field.
[FIELD].reset() () => void Reset [FIELD].current state to Provider.props.defaultValues[FIELD]. Clears any files, reader data, etc.
[FIELD].handleFileEvent(event: React.ChangeEvent<HTMLInputElement>) () => void Allow files to be referenced for a file change event. onChange={handleFileEvent}. *(see side effects)

Side effects

[FIELD].handleFileEvent(event: React.ChangeEvent<HTMLInputElement>)

This particular function will capture the event.target.files for use outside of the component in question.

Example

Controlled form

Basic login form with POST fetch using JSON payload.

const LoginForm = createForm<Login>({
  validate: {
    email: (value) => value.length === 0 ? 'Cannot be empty' : undefined,
    password: (value) => value.length === 0 ? 'Cannot be empty' : undefined,
  },
  props: {
    email: {
      type: 'email',
      placeholder: 
    }
  },
})

function Login() {
  return (
    <LoginForm.Provider defaultValues={{ email: '', password: '', session: false }}>
      <LoginEmail />
      <LoginEmailError />
      <LoginPassword />
      <LoginPasswordError />
      <SubmitButton />
    </LoginForm.Provider>
  )
}
function LoginEmail() {
  const { email } = useForm(LoginForm);
  return <input {...email} />
}
function LoginEmailError() {
  const { errors } = useForm(LoginForm);
  return (errors.email ? <span>{errors.email}</span> : null)
}
function LoginPassword() {
  const { password } = useForm(LoginForm);
  return <input {...password} />
}
function LoginPasswordError() {
  const { errors } = useForm(LoginForm);
  return (errors.password ? <span>{errors.password}</span> : null)
}
function SubmitButton() {
  const form = useForm(LoginForm);
  const handleClick = useCallback(() => {
    fetch('/login', { method: 'post', body: JSON.stringify(form.toJSON()) })
  }, [form])
  return <button onClick={handleClick}>Login</button>
}

Development

With Visual Studio Code, simply press F5 to start the development server and browser.

Run dev:

yarn start

Build prod:

Ctrl+shift+B in Visual Studio Code

yarn build

Run prod: (after build)

yarn start:server

Analyze production bundle sizes:

yarn build:analyze

Run with React Profiler:

yarn build:profile

Check Packages for duplicates or circular dependencies:

yarn pkgcheck

Run with Storybook:

yarn storybook

Share demo on Stackblitz

https://stackblitz.com/github/react-noui/create-form

About

React form library with very few opinions about your markup.

Resources

License

Stars

Watchers

Forks

Packages

No packages published