Quickstart

A quick dive into getting started with Lore

Step 4: Simplify the Dialog

In this step we're going to introduce a version of lore-hook-dialog that is tailored for mounting, showing, and dismissing Bootstrap dialogs.

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

What's the problem?

The dialog we just created is responsible for showing and dismissing itself. But in a real application, you may have dozens of dialogs for creating, updating and deleting content. And if your application uses a UI library like Bootstrap, each of those dialogs will likely have the exact same code for doing so.

So in this step, we're just going to embrace that, and we're going to introduce a version of lore-hook-dialog that has been customized to understand how to show and dismiss Bootstrap dialogs. This package is called lore-hook-dialog-bootstrap.

Add Bootstrap-specific Dialog Hook

Run this command to install the package:

npm install lore-hook-dialog-bootstrap --save

Next open index.js and replace the lore-hook-dialog hook with lore-hook-dialog-bootstrap like this:

// index.js
...
import dialog from 'lore-hook-dialog-bootstrap';
...

lore.summon({
  hooks: {
    ...
    dialog,
    ...
  }
});

With that change in place, the application will still work, but you'll notice that now when you launch the dialog, it now has a super dark backdrop that it didn't before.

Why does this happen?

This is happening because we're now creating two backdrops. Previously, our dialog needed to include code for showing and hiding itself, and it's this code (shown below) that also generates the backdrop:

// src/components/CreateTweetDialog.js
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';

export default createReactClass({
  displayName: 'CreateTweetDialog',

  componentDidMount() {
    this.show();
  },

  show() {
    const modal = this.refs.modal;
    $(modal).modal('show');
  },

  dismiss() {
    const modal = this.refs.modal;
    $(modal).modal('hide');
  },

  render() {
    const { data } = this.state;

    return (
      <div ref="modal" className="modal fade">
        {/* ...your dialog renders here... */}
      </div>
    );
  }

});
// src/components/CreateTweetDialog.js
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';

class CreateTweetDialog extends React.Component {

  constructor(props) {
    super(props);

    // bind custom methods
    this.show = this.show.bind(this);
    this.dismiss = this.dismiss.bind(this);
  }

  componentDidMount() {
    this.show();
  }

  show() {
    const modal = this.refs.modal;
    $(modal).modal('show');
  }

  dismiss() {
    const modal = this.refs.modal;
    $(modal).modal('hide');
  }

  render() {
    const { data } = this.state;

    return (
      <div ref="modal" className="modal fade">
        {/* ...your dialog renders here... */}
      </div>
    );
  }

}
// src/components/CreateTweetDialog.js
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';

class CreateTweetDialog extends React.Component {

  constructor(props) {
    super(props);

    // bind custom methods
    this.show = this.show.bind(this);
    this.dismiss = this.dismiss.bind(this);
  }

  componentDidMount() {
    this.show();
  }

  show() {
    const modal = this.refs.modal;
    $(modal).modal('show');
  }

  dismiss() {
    const modal = this.refs.modal;
    $(modal).modal('hide');
  }

  render() {
    const { data } = this.state;

    return (
      <div ref="modal" className="modal fade">
        {/* ...your dialog renders here... */}
      </div>
    );
  }

}

Since the new lore.dialog.show() method automatically wraps each dialog with this code for us, we can now remove this boilerplate from our dialog.

Remove Boilerplate from Dialog

Open the CreateTweetDialog and update it to look like this:

// src/components/CreateTweetDialog.js
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';

export default createReactClass({
  displayName: 'CreateTweetDialog',

  propTypes: {
    onCancel: PropTypes.func
  },

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

  request(data) {
    lore.actions.tweet.create(data);
  },

  onSubmit() {
    const { data } = this.state;
    this.request(data);
    this.dismiss();
  },

  dismiss() {
    this.props.onCancel();
  },

  onChange(name, value) {
    const nextData = _.merge({}, this.state.data);
    nextData[name] = value;
    this.setState({
      data: nextData
    });
  },

  render() {
    const { data } = this.state;

    return (
      <div className="modal-dialog">
        <div className="modal-content">
          <div className="modal-header">
            <button type="button" className="close" onClick={this.dismiss}>
              <span>&times;</span>
            </button>
            <h4 className="modal-title">
              Create Tweet
            </h4>
          </div>
          <div className="modal-body">
            <div className="row">
              <div className="col-md-12">
                <div className="form-group">
                  <label>Message</label>
                  <textarea
                    className="form-control"
                    rows="3"
                    value={data.text}
                    placeholder="What's happening?"
                    onChange={(event) => {
                      this.onChange('text', event.target.value)
                    }}
                  />
                </div>
              </div>
            </div>
          </div>
          <div className="modal-footer">
            <div className="row">
              <div className="col-md-12">
                <button
                  type="button"
                  className="btn btn-default"
                  onClick={this.dismiss}
                >
                  Cancel
                </button>
                <button
                  type="button"
                  className="btn btn-primary"
                  disabled={!data.text}
                  onClick={this.onSubmit}
                >
                  Create
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }

});
// src/components/CreateTweetDialog.js
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';

class CreateTweetDialog extends React.Component {

  constructor(props) {
    super(props);

    // set initial state
    this.state = {
      data: {
        text: ''
      }
    };

    // bind custom methods
    this.request = this.request.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.onChange = this.onChange.bind(this);
  }

  request(data) {
    lore.actions.tweet.create(data);
  }

  onSubmit() {
    const { data } = this.state;
    this.request(data);
    this.dismiss();
  }

  onChange(name, value) {
    const nextData = _.merge({}, this.state.data);
    nextData[name] = value;
    this.setState({
      data: nextData
    });
  }

  render() {
    const { data } = this.state;

    return (
      <div className="modal-dialog">
        <div className="modal-content">
          <div className="modal-header">
            <button type="button" className="close" onClick={this.dismiss}>
              <span>&times;</span>
            </button>
            <h4 className="modal-title">
              Create Tweet
            </h4>
          </div>
          <div className="modal-body">
            <div className="row">
              <div className="col-md-12">
                <div className="form-group">
                  <label>Message</label>
                  <textarea
                    className="form-control"
                    rows="3"
                    value={data.text}
                    placeholder="What's happening?"
                    onChange={(event) => {
                      this.onChange('text', event.target.value)
                    }}
                  />
                </div>
              </div>
            </div>
          </div>
          <div className="modal-footer">
            <div className="row">
              <div className="col-md-12">
                <button
                  type="button"
                  className="btn btn-default"
                  onClick={this.dismiss}
                >
                  Cancel
                </button>
                <button
                  type="button"
                  className="btn btn-primary"
                  disabled={!data.text}
                  onClick={this.onSubmit}
                >
                  Create
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }

}

CreateTweetDialog.propTypes = {
  title: PropTypes.node,
  description: PropTypes.node
};

export default CreateTweetDialog;
// src/components/CreateTweetDialog.js
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';

class CreateTweetDialog extends React.Component {

  static propTypes = {
    title: PropTypes.node,
    description: PropTypes.node
  };

  constructor(props) {
    super(props);

    // set initial state
    this.state = {
      data: {
        text: ''
      }
    };

    // bind custom methods
    this.request = this.request.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.onChange = this.onChange.bind(this);
  }

  request(data) {
    lore.actions.tweet.create(data);
  }

  onSubmit() {
    const { data } = this.state;
    this.request(data);
    this.dismiss();
  }

  onChange(name, value) {
    const nextData = _.merge({}, this.state.data);
    nextData[name] = value;
    this.setState({
      data: nextData
    });
  }

  render() {
    const { data } = this.state;

    return (
      <div className="modal-dialog">
        <div className="modal-content">
          <div className="modal-header">
            <button type="button" className="close" onClick={this.dismiss}>
              <span>&times;</span>
            </button>
            <h4 className="modal-title">
              Create Tweet
            </h4>
          </div>
          <div className="modal-body">
            <div className="row">
              <div className="col-md-12">
                <div className="form-group">
                  <label>Message</label>
                  <textarea
                    className="form-control"
                    rows="3"
                    value={data.text}
                    placeholder="What's happening?"
                    onChange={(event) => {
                      this.onChange('text', event.target.value)
                    }}
                  />
                </div>
              </div>
            </div>
          </div>
          <div className="modal-footer">
            <div className="row">
              <div className="col-md-12">
                <button
                  type="button"
                  className="btn btn-default"
                  onClick={this.dismiss}
                >
                  Cancel
                </button>
                <button
                  type="button"
                  className="btn btn-primary"
                  disabled={!data.text}
                  onClick={this.onSubmit}
                >
                  Create
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }

}

export default CreateTweetDialog;

With that change in place, if you launch your dialog again, it will look and behave like we expect, and will once again have only a single backdrop.

Visual Check-in

If everything went well, your application should now look like this.

Code Changes

Below is a list of files modified during this step.

index.js

/**
 * This file kicks off the build process for the application.  It also attaches
 * the Lore singleton to the window, so you can access it from the command line
 * in case you need to play with it or want to manually kick off actions or check
 * the reducer state (through `lore.actions.xyz`, `lore.reducers.xyz`,
 * `lore.models.xyz`, etc.)
 */

import lore from 'lore';
import _ from 'lodash';

// Import the styles for the loading screen. We're doing that here to make
// sure they get loaded regardless of the entry point for the application.
import './assets/css/loading-screen.css';

// Allows you to access your lore app globally as well as from within
// the console. Remove this line if you don't want to be able to do that.
window.lore = lore;

// Hooks
import auth from 'lore-hook-auth';
import actions from 'lore-hook-actions';
import bindActions from 'lore-hook-bind-actions';
import collections from 'lore-hook-collections';
import connections from 'lore-hook-connections';
import connect from 'lore-hook-connect';
import dialog from 'lore-hook-dialog-bootstrap';
import models from 'lore-hook-models';
import react from 'lore-hook-react';
import reducers from 'lore-hook-reducers';
import redux from 'lore-hook-redux';
import router from 'lore-hook-router';

// Summon the app!
lore.summon({
  hooks: {
    auth,
    actions,
    bindActions,
    collections,
    connections,
    connect,
    dialog,
    models,
    react,
    reducers,
    redux: _.extend(redux, {
      dependencies: ['reducers', 'auth']
    }),
    router
  }
});

src/components/CreateTweetDialog.js

// src/components/CreateTweetDialog.js
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';

export default createReactClass({
  displayName: 'CreateTweetDialog',

  propTypes: {
    onCancel: PropTypes.func
  },

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

  request(data) {
    lore.actions.tweet.create(data);
  },

  onSubmit() {
    const { data } = this.state;
    this.request(data);
    this.dismiss();
  },

  dismiss() {
    this.props.onCancel();
  },

  onChange(name, value) {
    const nextData = _.merge({}, this.state.data);
    nextData[name] = value;
    this.setState({
      data: nextData
    });
  },

  render() {
    const { data } = this.state;

    return (
      <div className="modal-dialog">
        <div className="modal-content">
          <div className="modal-header">
            <button type="button" className="close" onClick={this.dismiss}>
              <span>&times;</span>
            </button>
            <h4 className="modal-title">
              Create Tweet
            </h4>
          </div>
          <div className="modal-body">
            <div className="row">
              <div className="col-md-12">
                <div className="form-group">
                  <label>Message</label>
                  <textarea
                    className="form-control"
                    rows="3"
                    value={data.text}
                    placeholder="What's happening?"
                    onChange={(event) => {
                      this.onChange('text', event.target.value)
                    }}
                  />
                </div>
              </div>
            </div>
          </div>
          <div className="modal-footer">
            <div className="row">
              <div className="col-md-12">
                <button
                  type="button"
                  className="btn btn-default"
                  onClick={this.dismiss}
                >
                  Cancel
                </button>
                <button
                  type="button"
                  className="btn btn-primary"
                  disabled={!data.text}
                  onClick={this.onSubmit}
                >
                  Create
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }

});
// src/components/CreateTweetDialog.js
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';

class CreateTweetDialog extends React.Component {

  constructor(props) {
    super(props);

    // set initial state
    this.state = {
      data: {
        text: ''
      }
    };

    // bind custom methods
    this.request = this.request.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.onChange = this.onChange.bind(this);
  }

  request(data) {
    lore.actions.tweet.create(data);
  }

  onSubmit() {
    const { data } = this.state;
    this.request(data);
    this.dismiss();
  }

  onChange(name, value) {
    const nextData = _.merge({}, this.state.data);
    nextData[name] = value;
    this.setState({
      data: nextData
    });
  }

  render() {
    const { data } = this.state;

    return (
      <div className="modal-dialog">
        <div className="modal-content">
          <div className="modal-header">
            <button type="button" className="close" onClick={this.dismiss}>
              <span>&times;</span>
            </button>
            <h4 className="modal-title">
              Create Tweet
            </h4>
          </div>
          <div className="modal-body">
            <div className="row">
              <div className="col-md-12">
                <div className="form-group">
                  <label>Message</label>
                  <textarea
                    className="form-control"
                    rows="3"
                    value={data.text}
                    placeholder="What's happening?"
                    onChange={(event) => {
                      this.onChange('text', event.target.value)
                    }}
                  />
                </div>
              </div>
            </div>
          </div>
          <div className="modal-footer">
            <div className="row">
              <div className="col-md-12">
                <button
                  type="button"
                  className="btn btn-default"
                  onClick={this.dismiss}
                >
                  Cancel
                </button>
                <button
                  type="button"
                  className="btn btn-primary"
                  disabled={!data.text}
                  onClick={this.onSubmit}
                >
                  Create
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }

}

CreateTweetDialog.propTypes = {
  title: PropTypes.node,
  description: PropTypes.node
};

export default CreateTweetDialog;
// src/components/CreateTweetDialog.js
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';

class CreateTweetDialog extends React.Component {

  static propTypes = {
    title: PropTypes.node,
    description: PropTypes.node
  };

  constructor(props) {
    super(props);

    // set initial state
    this.state = {
      data: {
        text: ''
      }
    };

    // bind custom methods
    this.request = this.request.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.onChange = this.onChange.bind(this);
  }

  request(data) {
    lore.actions.tweet.create(data);
  }

  onSubmit() {
    const { data } = this.state;
    this.request(data);
    this.dismiss();
  }

  onChange(name, value) {
    const nextData = _.merge({}, this.state.data);
    nextData[name] = value;
    this.setState({
      data: nextData
    });
  }

  render() {
    const { data } = this.state;

    return (
      <div className="modal-dialog">
        <div className="modal-content">
          <div className="modal-header">
            <button type="button" className="close" onClick={this.dismiss}>
              <span>&times;</span>
            </button>
            <h4 className="modal-title">
              Create Tweet
            </h4>
          </div>
          <div className="modal-body">
            <div className="row">
              <div className="col-md-12">
                <div className="form-group">
                  <label>Message</label>
                  <textarea
                    className="form-control"
                    rows="3"
                    value={data.text}
                    placeholder="What's happening?"
                    onChange={(event) => {
                      this.onChange('text', event.target.value)
                    }}
                  />
                </div>
              </div>
            </div>
          </div>
          <div className="modal-footer">
            <div className="row">
              <div className="col-md-12">
                <button
                  type="button"
                  className="btn btn-default"
                  onClick={this.dismiss}
                >
                  Cancel
                </button>
                <button
                  type="button"
                  className="btn btn-primary"
                  disabled={!data.text}
                  onClick={this.onSubmit}
                >
                  Create
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }

}

export default CreateTweetDialog;

Next Steps

Next we're going finish adding the create dialog.