Forms: Pattern Construction

WARNING! v0.13 was just released, and the tutorial is currently undergoing final testing. It's recommended that you DO NOT follow along until this message is removed (please check back tomorrow).

Mental model for the forms implementation

1. Refactor Delete Dialog

In this step we'll be refactoring the UpdateTweetDialog to leverage the blueprint we just created.

You can view the finished code for this step by checking out the destroy.1 branch of the completed project.

Dialog Comparison

If you compare the delete dialog to the original update dialog, you'll find that once again, very little is different. In fact, the only code that's different is reflected below:

...
propTypes: {
  tweet: PropTypes.object.isRequired
},

getInitialState() {
  return {
    data: {}
  };
},

request(data) {
  const { tweet } = this.props;
  lore.actions.tweet.update(tweet, data);
},

getValidators: function(data) {
  return {};
},

render() {
  ...
  return (
    <div className="modal-dialog">
        ...
          <h4 className="modal-title">
            Delete Tweet
          </h4>
        ...
        <div className="modal-body">
          <div className="row">
            <div className="col-md-12">
              <p>
                Are you sure you want to delete this?
              </p>
            </div>
          </div>
        </div>
        <div className="modal-footer">
          ...
          <button
            type="button"
            className="btn btn-primary"
            disabled={hasError}
            onClick={this.onSubmit}
          >
            Delete
          </button>
          ...
        </div>
        ...
    </div>
  );
}
..

In this case, the delete dialog is intended to delete a tweet, and so it requires a prop which is the tweet that should be updated.

The biggest difference is that instead of fields for user input, this form prompts the user with a confirmation question, to double check that they mean to delete the tweet. Once they confirm, and the form is submitted, the request() method uses the delete action instead of the update action.

Also, because the form has no user input fields, the getInitialState() does not need to set an initial state for the form, and there is no need to specify an validators.

Additionally, (looking at the render() method) the title of the form is "Delete Tweet" instead of "Update Tweet", and the onSubmit button says "Delete" instead of "Update".

Other than that, the forms are identical.

Add Custom Field Type

Currently, we can solve all of those requirements except for inserting custom text into a form. So how do we solve that?

One way is to think of custom text like the confirmation question as a type of field, but one that allows you to render whatever you want, instead of predetermined "thing", like we do with the text and select fields. And luckily, our current setup will allow us to do that.

To demonstrate how to do this, open config/dialogs.js. Then find the section that defines the fieldMap, and add a new field type called custom that looks like this:

// config/dialogs.js
...
fieldMap: {
  ...
  custom: function(form, props, name) {
    return (
      <Field key={name} name={name} data={form.data} errors={form.errors} onChange={form.onChange}>
        {(field) => {
          return props.render(field, props);
        }}
      </Field>
    );
  }
}
...

With that change in place, we can now specify insert custom fields into forms, as long as we provide a render() method as a prop to that field type, we can render whatever we want.

Refactor Delete Dialog

To see how to use the new field type we just created, open the delete dialog located at src/dialogs/tweet/destroy/index.js and replace the contents with the code shown below:

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import OptimisticBlueprint from '../../_blueprints/Optimistic';

export default createReactClass({
  displayName: 'DeleteTweetDialog',

  propTypes: {
    onCancel: PropTypes.func,
    tweet: PropTypes.object.isRequired
  },

  getInitialState() {
    return {
      data: {}
    };
  },

  request(data) {
    const { tweet } = this.props;
    lore.actions.tweet.destroy(tweet);
  },

  render() {
    const { data } = this.state;
    const { schema, fieldMap, actionMap } = lore.config.dialogs;

    return (
      <OptimisticBlueprint
        title="Delete Tweet"
        onCancel={this.props.onCancel}
        data={data}
        schema={schema}
        fieldMap={fieldMap}
        actionMap={actionMap}
        request={this.request}
        validators={function(data) {
          return {};
        }}
        fields={[
          {
            key: 'confirmation',
            type: 'custom',
            props: {
              render: (form) => {
                return (
                  <p>
                    Are you sure you want to delete this?
                  </p>
                );
              }
            }
          }
        ]}
        actions={[
          {
            key: 'cancel',
            type: 'default',
            props: (form) => {
              return {
                label: 'Cancel',
                onClick: form.callbacks.dismiss
              };
            }
          },
          {
            key: 'submit',
            type: 'primary',
            props: (form) => {
              return {
                label: 'Delete',
                disabled: form.hasError,
                onClick: form.callbacks.onSubmit
              };
            }
          }
        ]}
      />
    );
  }

});

Here we've specified one field, of type custom, and then provided a render() method via the props we want passed to that component during creation. In this case, the fucntion we defined for the custom field type looks for that function, and then renders what ever it returns.

Visual Check-in

If everything went well, clicking the delete link will still launch a dialog that you can use to delete an existing tweet.

Code Changes

Below is a list of files modified during this step.

src/dialogs/tweet/destroy/index.js

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import OptimisticBlueprint from '../../_blueprints/Optimistic';

export default createReactClass({
  displayName: 'DeleteTweetDialog',

  propTypes: {
    onCancel: PropTypes.func,
    tweet: PropTypes.object.isRequired
  },

  getInitialState() {
    return {
      data: {}
    };
  },

  request(data) {
    const { tweet } = this.props;
    lore.actions.tweet.destroy(tweet);
  },

  render() {
    const { data } = this.state;
    const { schema, fieldMap, actionMap } = lore.config.dialogs;

    return (
      <OptimisticBlueprint
        title="Delete Tweet"
        onCancel={this.props.onCancel}
        data={data}
        schema={schema}
        fieldMap={fieldMap}
        actionMap={actionMap}
        request={this.request}
        validators={function(data) {
          return {};
        }}
        fields={[
          {
            key: 'confirmation',
            type: 'custom',
            props: {
              render: (form) => {
                return (
                  <p>
                    Are you sure you want to delete this?
                  </p>
                );
              }
            }
          }
        ]}
        actions={[
          {
            key: 'cancel',
            type: 'default',
            props: (form) => {
              return {
                label: 'Cancel',
                onClick: form.callbacks.dismiss
              };
            }
          },
          {
            key: 'submit',
            type: 'primary',
            props: (form) => {
              return {
                label: 'Delete',
                disabled: form.hasError,
                onClick: form.callbacks.onSubmit
              };
            }
          }
        ]}
      />
    );
  }

});

Next Steps

This step concludes the tutorial. Next we're going to discuss next steps.