(note (code cslai))

Building Dynamic Form the hard way with React-Redux

Javascript is getting so foreign to me these days, but mostly towards a better direction. So I recently got myself to learn react through work and the JSX extension makes web development bearable again. On the other hand, I picked up a little bit on Vue.js but really hated all the magic involved (No I don’t enjoy putting in code into quotes).

Building a dynamic form that has components appearing or not appearing depends on the other field value is a very annoying problem. While learning React I was told about redux and I am instantly sold on the idea (perhaps because my preferred programming paradigm is functional style). Then I started building my current project at work using these. While some parts are made easier, but it is still overall a confusing thing to do.

This post is intended to be a quick note on how I am approaching to the problem currently, and may not be the best way (part of it violates the jsx-no-bind lint rule for instance, also I still don’t do camelCase).

Problem statement

Suppose I want to build a form that generates a list of publications of any length. And depending on the type of the publication it would take one of the following two forms.

{
    "title": "Becoming Jeffrey04",
    "authors": [
        {
            "name": "Jeffrey",
            "website": "http://coolsilon.com/"
        }
    ],
    "type": "journal"
    "meta": {
        "volume": "0",
        "editors": [
            {
                "name": "John Doe",
                "website": "http://john.coolsilon.com/"
            }
        ]
    }
}

or

{
    "title": "Becoming Jeffrey04",
    "authors": [
        {
            "name": "Jeffrey",
            "website": "http://coolsilon.com/"
        }
    ],
    "type": "book"
    "meta": {
        "title": "I am the best!",
        "editors": [
            {
                "name": "John Doe",
                "website": "http://john.coolsilon.com/"
            }
        ],
        "is_published": false
    }
}

So the editors and authors are both to be of any length.

One of the thing I like about redux is that I don’t have to worry about UI update IN ADDITION to the state, which is usually stored elsewhere. Back then in my jQuery days (which you can probably see in my past work) I needed to remember to save the state, and then notify the widget to update accordingly.

Not anymore, now I just need to update the state and redux would trigger the UI rendering where needed. This is going to be very useful when we have a dynamic form because I don’t have to worry about whether the fields are properly updated after every click or keystrokes.

So we start from the base publication list. So while building the second dynamic form I start to find ways to do it better and clearer (and may or may not have time to fix the previous one to the new way).

Some boring convention talk

So in building dynamic form there are basically 2 different types of important functions/methods that one needs to write. First is to handle onChange or onClick events, and the other to compute current state of the form to be sent to redux.

Did I mention the programming style is very personal before this?

The first type of function, which handles the user input events, I am going to name them as handle_$event[_$name] where $event is the type of event, and the option $name is used when there are multiple events happening in the same component. They are also usually the one who calls the redux dispatcher.

Secondly the function that computes the state, to be passed down to child components to help generate complete snapshot of the form.These functions are going to be named as handle_update and get passed down as delegate_update property. Unless otherwise specified, the function signature is either one of the following two.

The second form is usually the one that violates the jsx-no-bind rule, just for your information.

This posts assumes knowledge of react-redux.

There are also 3 different types of component.

We are also going to ignore the third type. The second type of component is usually where you see the declaring of second form of handle_update.

This post is getting long, I am going to split it into multiple pages.

First component

Let’s start from a component that allow the input of multiple publications. So there should always be 1 empty form for a publication, and the user can click on something to add another. Then the user should also be able to remove them as wished.

class AppWidget extends Component {
  constructor(props) {
    super(props);

    this.handle_click = props.handle_click.bind(this);
  }

  render() {
    return (
      <fieldset>
        <legend>Publications</legend>

        {this.props.publications.map((item, idx) => (
          <Publication
            key={idx}
            publication={item}
            delegate_update={this.props.handle_update.bind(this, idx)}
          />
        ))}

        <a onClick={this.handle_click} href="#">
          Add new publiations
        </a>
      </fieldset>
    );
  }
}

(I am going to show only the render function in the following examples)

So the code above is very much only for presentation purpose, nothing much to talk about. It lists all the available publications. Then there’s also a link to click so a new publication form is added to the list.

Then we have the respective container created through react-redux connect function.

export default connect(
  state => ({
    publications: state.publications || [{}]
  }),
  dispatch => ({
    handle_click(e) {
      e.preventDefault();

      dispatch(make_publication_update(this.props.publications.concat([{}])));
    },

    handle_update(position, is_delete, changes) {
      return make_publication_update(
        is_delete
          ? this.props.publications.filter((_, idx) => idx !== position)
          : this.props.publications.map(
              (_current, idx) =>
                idx === position
                  ? Object.assign({}, _current, changes)
                  : _current
            )
      );
    }
  })
)(AppWidget);

Remember in the previous page I mentioned I only call dispatch in the browser event handlers. In this case I am concatenating a new empty publication to the list, send it to the action creator, and dispatch the action.

The more interesting part is the handle_update function here. It is of the second form, which has a position required in the function parameters. I could have passed the position as a property to the component, however I decided in the end not to because the component doesn’t have to know its position to serve its purpose, plus this would allow it to be used elsewhere when it is not in a list. This is why I bound the position in before sending it in as delegate_update property.

This also indirectly makes all delegate_update property share the same function call signature, which is delegate_update(is_delete, changes)

The parameter is_delete is placed in the first because whenever a delete operations is needed it would work with only 1 argument passed in. We shall see it in action in the following Publication component.

Single Entity Component

So the previous section was about a component that lists all the entities stored in an array, and it has the ability to add in new one through a click of a link. Now we would have to actually display a form to allow user to edit ONE publication.

  render() {
    return (
      <fieldset>
        <legend>Publication</legend>
        <p>
          <label>
            title<br />
            <input
              onChange={this.handle_change}
              name="title"
              value={this.props.publication.title}
            />
          </label>
        </p>

        <Persons
          legend="Authors"
          field="authors"
          delegate_update={this.handle_update}
          persons={this.props.publication.authors || [{}]}
        />

        <p>
          <label>
            Type<br />
            <select name="type" onChange={this.handle_change}>
              <option disabled selected={this.props.publication.type || true}>
                select
              </option>
              <option
                value="journal"
                selected={this.props.publication.type === "journal"}
              >
                Journal
              </option>
              <option
                selected={this.props.publication.type === "book"}
                value="book"
              >
                book
              </option>
            </select>
          </label>
        </p>

        <p>{this.get_meta_widget()}</p>

        <p>
          <a onClick={this.handle_click} href="#">
            Delete this publication
          </a>
        </p>
      </fieldset>
    );
  }

We talk about this.get_meta_widget() later.

So in each publication, we can have a list of authors of arbitrary length as well, but we don’t do the listing here, and instead delegate the work to another component, and passing only handle_update of the first form mentioned in the first section.

Then we also have a delete button to remove this exact publication.

Here is the respective methods in the container.

export default connect(
  state => ({}),
  dispatch => ({
    handle_change(e) {
      e.preventDefault();

      dispatch(
        this.props.delegate_update(false, {
          [e.target.getAttribute("name")]: e.target.value
        })
      );
    },

    handle_click(e) {
      e.preventDefault();

      dispatch(this.props.delegate_update(true));
    },

    handle_update(_, changes) {
      return this.props.delegate_update(
        false,
        Object.assign({}, this.props.publication, changes)
      );
    }
  })
)(PublicationWidget);

In order to delete itself, the component just needed to call delegate_update(true) without having to pass extra parameters to the function. It doesn’t need to know where it is in a list (because it was bound to the handle_update method.

OTOH when a change is happening, it needs to explicitly tell that it is not a deletion, as well as passing in the update to the original state. Good thing about the react-redux integration here is that I don’t have to worry about updating the input element because they would be reflected when a re-render takes place (I love this part so much I am going to mention it again).

Lastly the handle_update is done mainly for the component that lists authors. You will probably see it in action again later.

Showing different sub-form based on user input

Remember there was a call to this.get_meta_widget?

So again, the advantage of using redux is that it always keep the latest state of the application. By using react-redux, it always re-render the components that receives a different set of properties.

Here is what get_meta_widget does

  get_meta_widget() {
    let result;

    switch (this.props.publication.type) {
      case "journal":
        result = (
          <Journal
            delegate_update={this.handle_update}
            definition={this.props.publication.meta || {}}
          />
        );
        break;
      case "book":
        result = (
          <Book
            delegate_update={this.handle_update}
            definition={this.props.publication.meta || {}}
          />
        );
        break;
      default:
        result = "";
    }
  get_meta_widget() {
    let result;

    switch (this.props.publication.type) {
      case "journal":
        result = (
          <Journal
            delegate_update={this.handle_update}
            definition={this.props.publication.meta || {}}
          />
        );
        break;
      case "book":
        result = (
          <Book
            delegate_update={this.handle_update}
            definition={this.props.publication.meta || {}}
          />
        );
        break;
      default:
        result = "";
    }

    return result;
  }
    return result;
  }

There you have it, there’s no need to notify the component when the user made changes through the select box (refer to the previous page). As the state changes, the component will re-render itself, and a different component is then inserted into the form automatically.

On the component reuse

So in the publication meta field (refer to the problem statement in the first page, there is another list of people, this time as editors. So we can now insert the Persons component, very similar to what we did with authors previously (in the previous page).

        <Persons
          delegate_update={this.handle_update}
          field="editors"
          legend="Editors"
          persons={this.props.definition.editors || [{}]}
        />

and the respective handle_update

    handle_update(_, changes) {
      return this.props.delegate_update(false, {
        meta: Object.assign({}, this.props.definition, changes)
      });
    }

Which is very similar to what we did in other components. It is just the matter of generating a new entity via Object.assign calls, integrate the changes to the parent form through delegate_update property method. Then the child component can generate the new state without having to know what the parent component looks like, then dispatch the new revision to redux.

The complete code can be found at my github repository. Feel free to leave me a comment here or an issue on github so we can discuss further into this.

Exit mobile version