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 Update 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 update.1 branch of the completed project.

Dialog Comparison

If you compared the original create dialog with the update dialog, very little is different. In fact, the only code that's different is reflected below:

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

getInitialState() {
  const { tweet } = this.props;

  return {
    data: {
      text: tweet.data.text,
      userId: tweet.data.userId
    }
  };
},

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

render() {
  ...
  return (
    <div className="modal-dialog">
        ...
          <h4 className="modal-title">
            Update Tweet
          </h4>
        ...
        <div className="modal-footer">
          ...
          <button
            type="button"
            className="btn btn-primary"
            disabled={hasError}
            onClick={this.onSubmit}
          >
            Update
          </button>
          ...
        </div>
        ...
    </div>
  );
}
..

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

The getInitialState() method then uses that tweet to set the initial state of the form, and when the form is submitted, the request() method uses the update action instead of the create action.

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

Other than that, the forms are identical.

Refactor Update Dialog

Lucky for us, it turns out that none of the differences captured above affect the blueprint, which means we can reuse it for the update dialog.

Open the update dialog located at src/dialogs/tweet/update/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: 'UpdateTweetDialog',

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

  getInitialState() {
    const { tweet } = this.props;

    return {
      data: {
        text: tweet.data.text,
        userId: tweet.data.userId
      }
    };
  },

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

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

    return (
      <OptimisticBlueprint
        title="Update Tweet"
        onCancel={this.props.onCancel}
        data={data}
        schema={schema}
        fieldMap={fieldMap}
        actionMap={actionMap}
        request={this.request}
        validators={function(data) {
          return {
            text: [function(value) {
              if (!value) {
                return 'This field is required';
              }
            }],
            userId: [function(value) {
              if (value === undefined) {
                return 'This field is required'
              }
            }]
          };
        }}
        fields={[
          {
            key: 'text',
            type: 'text',
            props: {
              label: 'Message',
              placeholder: "What's happening?"
            }
          },
          {
            key: 'userId',
            type: 'select',
            props: {
              label: 'User',
              options: function(getState) {
                return getState('user.find');
              },
              optionLabel: 'nickname'
            }
          }
        ]}
        actions={[
          {
            key: 'cancel',
            type: 'default',
            props: (form) => {
              return {
                label: 'Cancel',
                onClick: form.callbacks.dismiss
              };
            }
          },
          {
            key: 'submit',
            type: 'primary',
            props: (form) => {
              return {
                label: 'Update',
                disabled: form.hasError,
                onClick: form.callbacks.onSubmit
              };
            }
          }
        ]}
      />
    );
  }

});

While we won't do in this tutorial, if you compare the create and update dialogs at this point, you'll notice the validators and fields are identical. And if you wanted, you could even move that part of the code into a location both dialogs could share, to further reduce code.

Visual Check-in

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

Code Changes

Below is a list of files modified during this step.

src/dialogs/tweet/update/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: 'UpdateTweetDialog',

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

  getInitialState() {
    const { tweet } = this.props;

    return {
      data: {
        text: tweet.data.text,
        userId: tweet.data.userId
      }
    };
  },

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

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

    return (
      <OptimisticBlueprint
        title="Update Tweet"
        onCancel={this.props.onCancel}
        data={data}
        schema={schema}
        fieldMap={fieldMap}
        actionMap={actionMap}
        request={this.request}
        validators={function(data) {
          return {
            text: [function(value) {
              if (!value) {
                return 'This field is required';
              }
            }],
            userId: [function(value) {
              if (value === undefined) {
                return 'This field is required'
              }
            }]
          };
        }}
        fields={[
          {
            key: 'text',
            type: 'text',
            props: {
              label: 'Message',
              placeholder: "What's happening?"
            }
          },
          {
            key: 'userId',
            type: 'select',
            props: {
              label: 'User',
              options: function(getState) {
                return getState('user.find');
              },
              optionLabel: 'nickname'
            }
          }
        ]}
        actions={[
          {
            key: 'cancel',
            type: 'default',
            props: (form) => {
              return {
                label: 'Cancel',
                onClick: form.callbacks.dismiss
              };
            }
          },
          {
            key: 'submit',
            type: 'primary',
            props: (form) => {
              return {
                label: 'Update',
                disabled: form.hasError,
                onClick: form.callbacks.onSubmit
              };
            }
          }
        ]}
      />
    );
  }

});

Next Steps

Next we're going to refactor the delete dialog.