DEV Community

Muhammad Ali
Muhammad Ali

Posted on • Edited on

Managing list of form fields with formik through example

Managing sign-in / sign-up form is not that difficult, everyone does it, but what if you've an editable list to manage which has hundreds of items, e.g. a todo app? Worry not formik is to the rescue.

Why Formik

  1. Getting values in and out of form state is very easy and straightforward
  2. Handles form submission, validation and error messages very well
  3. Keeps form state local
  4. I'm not in mood of crying 😭

What you're going to learn

  1. Create editable HTML tag using contentEditable prop
  2. Formik's useField hook
  3. FieldArray component to manage list

Getting Started

Let's create a basic component, i.e. TodoApp.jsx, for our todo list app:

const INITIAL_TODOS = [
  { id: 1, content: "learn react", isCompleted: true },
  { id: 2, content: "learn react hooks", isCompleted: true },
  { id: 3, content: "learn formik", isCompleted: false }
];

const TodoItem = ({ content, isCompleted }) => (
  <div className={["todo-item", isCompleted ? "completed" : ""].join(" ")}>
    <input type="checkbox" checked={isCompleted} />
    <span contentEditable={true} className="todo-text">{content}</span>
  </div>
);

export const TodoApp = () => {
  return INITIAL_TODOS.map(todo => (
    <TodoItem
      key={todo.id}
      content={todo.content}
      isCompleted={todo.isCompleted}
    />
  ));
};

We've 3 todo items along with checkboxes and their content, a checkbox shows if a todo item is complete or not.
Everything is same old React except contentEditable prop which is doing some magic, right? Well, it basically make content of an HTML tag editable whether it's text or anything else. We'll see it's real usage in next couple of code snippets.
Let's add some basic styling for todo items:

.todo-item {
  display: flex;
  border: 1px dashed #999;
  margin: 5px 0;
  padding: 5px;
}
.todo-item.completed {
  text-decoration: line-through;
  background: #80eec5;
}
.todo-text {
  flex-grow: 1;
  margin-left: 10px;
  min-height: 20px;
  /* Removes outline when using contentEditable */
  outline: none;
  overflow: hidden;
  word-break: break-word;
}

The one with Formik

Run yarn add formik or npm i --save formik in your project repo.
We're going to wrap our todo items with Formik.

import { Formik } from "formik";

export const TodoApp = () => (
  <Formik initialValues={{ todos: INITIAL_TODOS }}>
    {formikProps => {
      const { values } = formikProps;

      return values.todos.map((todo, index) => (
        <TodoItem key={todo.id} index={index} />
      ));
    }}
  </Formik>
);

Nothing has happened just yet in actual but we've successfully integrated formik with our tiny TodoApp.

The one with useField

We've to change TodoItem component now as we're passing index of the array in props.

import { useField } from "formik";

const TodoItem = ({ index }) => {
  // Binding `isCompleted` using index of todos array
  const [completedField] = useField({ name: `todos[${index}].isCompleted`, type: "checkbox" });
  // Binding `content` using index of todos array
  const [contentField, contentMeta, contentHelpers] = useField(`todos[${index}].content`);
  const onContentInput = e => {
    contentHelpers.setValue(e.currentTarget.innerText);
  };
  const onContentBlur = () => {
    contentHelpers.setTouched(true);
  };

  return (
    <div
      className={["todo-item", completedField.value ? "completed" : ""].join(" ")}
    >
      <input
        type="checkbox"
        name={completedField.name}
        checked={completedField.checked}
        onChange={({ target }) => {
          completedHelpers.setValue(target.checked);
          // As type=checkbox doesn't call onBlur event
          completedHelpers.setTouched(true);
        }}
      />
      <span
        contentEditable={true}
        className={"todo-text"}
        onInput={onContentInput}
        onBlur={onContentBlur}
      >
        {/*
         * We must set html content through `contentMeta.initialValue`,
         * because `contentField.value` will be updated upon `onChange | onInput`
         * resulting in conflicts between states of content. As 1 will be managed by
         * React and other with contentEditable prop.
         */}
        {contentField.value}
      </span>
    </div>
  );
};

Custom hooks are now part of formik >= v2, useField hook returns a 3-tuple (an array with three elements) containing FieldProps, FieldMetaProps and FieldHelperProps. It accepts either a string of a field name or an object as an argument. The object must at least contain a name key. You can read more about useField here.

The one with FieldArray

Enough with the static data, let's dig a bit deeper and create an Add button for dynamically creating todo items. To do that we can make use of FieldArray. FieldArray is a component that helps with common array/list manipulations. You pass it a name property with the path to the key within values that holds the relevant array, i.e. todos. FieldArray will then give you access to array helper methods via render props.

Common array helper methods:

  • push: (obj: any) => void: Add a value to the end of an array
  • swap: (indexA: number, indexB: number) => void: Swap two values in an array
  • move: (from: number, to: number) => void: Move an element in an array to another index
  • remove<T>(index: number): T | undefined: Remove an element at an index of an array and return it

To read more about FieldArray visit official documentation.

import { Form, Formik, FieldArray } from "formik";

export const TodoApp = () => (
  <Formik initialValues={{ todos: [] }}>
    <Form>
      {/* Pass name of the array, i.e. `todos` */}
      <FieldArray name="todos">
        {({ form, ...fieldArrayHelpers }) => {
          const onAddClick = () => {
            fieldArrayHelpers.push({
              id: values.todos.length,
              content: "",
              isCompleted: false
            });
          };

          return (
            <React.Fragment>
              <button onClick={onAddClick}>Add Item</button>
              {form.values.todos.map(({ id }, index) => (
                <TodoItem key={id} index={index} />
              ))}
            </React.Fragment>
          );
        }}
      </FieldArray>
    </Form>
  </Formik>
);

There you go, it was that simple, you've a working Todo app.
😆
You can add more features like removing the completed items but that's totally up to you.

          ...
const onRemoveClick = () => {
  form.setFieldValue(
    "todos",
    form.values.todos.filter(todo => !todo.isCompleted)
  );
};

<button onClick={onRemoveClick}>Remove</button>
          ...

I've skipped the validation part in this article as it was pretty simple but it's all implemented in the sandbox embedded below:

Most of the documentation part is taken from formik's official documentation and big thanks to Jared Palmer for all of the efforts.

Top comments (1)

Collapse
 
alejandrofs profile image
Jorge Alejandro Frias Salceda

Great tutorial! I now understand more about Formik.