Quickstart

A quick dive into getting started with Lore

Step 6: Add Edit Dialog

In this step we're going to add an "edit" link to tweets that, when clicked, will launch a dialog to edit the text.

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

The Update Action

The dialog we'll create in this step will allow us to edit a tweet, and we'll be invoking the update action when it's time to send our changes to the API. Assuming you have a tweet you want to change, you invoke the action like this:

lore.actions.tweet.update(tweet, {
  text: 'Different text'
})

The first argument is the tweet you want to update, and the second argument is the set of attributes you want to change. In this example, we're changing the text of the tweet from whatever it current is to "Different text".

If we assume the id of this tweet is 1, then invoking this action will send a PUT request to http://localhost:1337/tweets/1 and include our new properties in the body of the request.

You can learn more about the update action here.

Create Edit Link

Run this command to create a component for our edit link:

lore generate component EditLink

Then update the component to look like this:

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';

export default createReactClass({
  displayName: 'EditLink',

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

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

    lore.dialog.show(function() {
      return lore.dialogs.tweet.update(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.update(tweet, data).payload;
        }
      });
    });
  },

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        edit
      </a>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';

class EditLink extends React.Component {

  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }

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

    lore.dialog.show(function() {
      return lore.dialogs.tweet.update(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.update(tweet, data).payload;
        }
      });
    });
  }

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        edit
      </a>
    );
  }

}

EditLink.propTypes = {
  tweet: PropTypes.object.isRequired
};

export default EditLink;
import React from 'react';
import PropTypes from 'prop-types';

class EditLink extends React.Component {

  static propTypes = {
    tweet: PropTypes.object.isRequired
  };

  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }

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

    lore.dialog.show(function() {
      return lore.dialogs.tweet.update(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.update(tweet, data).payload;
        }
      });
    });
  }

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        edit
      </a>
    );
  }

}

export default EditLink;

In the code above, we're rendering a link with an onClick callback. When clicked, we'll show the dialog, and then invoke the update action when the data is submitted.

Unlike the create dialog, the update dialog requires you to provide the tweet you want to update as the first argument. This is required in order to populate the dialog with the current data.

Add an Edit Link to the Tweet

Next we want to add the edit link to each tweet. Open the Tweet component and update the render() method to look like this:

// src/components/Tweet.js
...
import EditLink from './EditLink';

...
  render() {
    ...
    return (
      <li className="list-group-item tweet">
        <div className="image-container">
          <img
            className="img-circle avatar"
            src={user.data.avatar} />
        </div>
        <div className="content-container">
          <h4 className="list-group-item-heading title">
            {user.data.nickname}
          </h4>
          <h4 className="list-group-item-heading timestamp">
            {'- ' + timestamp}
          </h4>
          <p className="list-group-item-text text">
            {tweet.data.text}
          </p>
          <div className="tweet-actions">
            <EditLink tweet={tweet} />
          </div>
        </div>
      </li>
    );
  }
...

With this change in place, refresh the browser and you should see an "edit" link on each of the tweets.

Describe the Update Fields

If you click the "edit" link, you'll notice the dialog that opens says "No fields have been provided". Similar to the create dialog, we need to describe the fields that should be displayed.

To fix this, open the tweet model and update the code to look like this:

// src/models/tweet.js
const fields = {
  data: {
    text: ''
  },
  validators: {
    text: [function(value) {
      if (!value) {
        return 'This field is required';
      }
    }]
  },
  fields: [
    {
      key: 'text',
      type: 'text',
      props: {
        label: 'Message',
        placeholder: "What's happening?"
      }
    }
  ]
};

export default {
  dialogs: {
    create: fields,
    update: fields
  }
}

In the code above, we're extracting the "create" fields into a variable called fields that we can reuse, and then using it to describe the "update" fields as well.

Try it Out

With this change in place, refresh the browser and click one of the "edit" links.

A dialog will appear allowing you to change the text. Once you submit the form, if you look at the network requests, you'll see a PUT request is sent to the API to update the tweet.

The state of the tweet is also changed to UPDATING, so if this were a real application, we could add an if statement to detect when data was being changed and modify our UI to communicate that to the user.

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.

src/components/EditLink.js

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';

export default createReactClass({
  displayName: 'EditLink',

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

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

    lore.dialog.show(function() {
      return lore.dialogs.tweet.update(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.update(tweet, data).payload;
        }
      });
    });
  },

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        edit
      </a>
    );
  }

});
import React from 'react';
import PropTypes from 'prop-types';

class EditLink extends React.Component {

  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }

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

    lore.dialog.show(function() {
      return lore.dialogs.tweet.update(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.update(tweet, data).payload;
        }
      });
    });
  }

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        edit
      </a>
    );
  }

}

EditLink.propTypes = {
  tweet: PropTypes.object.isRequired
};

export default EditLink;
import React from 'react';
import PropTypes from 'prop-types';

class EditLink extends React.Component {

  static propTypes = {
    tweet: PropTypes.object.isRequired
  };

  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }

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

    lore.dialog.show(function() {
      return lore.dialogs.tweet.update(tweet, {
        blueprint: 'optimistic',
        request: function(data) {
          return lore.actions.tweet.update(tweet, data).payload;
        }
      });
    });
  }

  render() {
    return (
      <a className="link" onClick={this.onClick}>
        edit
      </a>
    );
  }

}

export default EditLink;

src/components/Tweet.js

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import moment from 'moment';
import { connect } from 'lore-hook-connect';
import EditLink from './EditLink';

export default connect(function(getState, props) {
  const { tweet } = props;

  return {
    user: getState('user.byId', {
      id: tweet.data.user
    })
  };
})(
createReactClass({
  displayName: 'Tweet',

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

  render() {
    const { tweet, user } = this.props;
    const timestamp = moment(tweet.data.createdAt).fromNow().split(' ago')[0];

    return (
      <li className="list-group-item tweet">
        <div className="image-container">
          <img
            className="img-circle avatar"
            src={user.data.avatar} />
        </div>
        <div className="content-container">
          <h4 className="list-group-item-heading title">
            {user.data.nickname}
          </h4>
          <h4 className="list-group-item-heading timestamp">
            {'- ' + timestamp}
          </h4>
          <p className="list-group-item-text text">
            {tweet.data.text}
          </p>
          <div className="tweet-actions">
            <EditLink tweet={tweet} />
          </div>
        </div>
      </li>
    );
  }

})
);
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { connect } from 'lore-hook-connect';
import EditLink from './EditLink';

class Tweet extends React.Component {

  render() {
    const { tweet, user } = this.props;
    const timestamp = moment(tweet.data.createdAt).fromNow().split(' ago')[0];

    return (
      <li className="list-group-item tweet">
        <div className="image-container">
          <img
            className="img-circle avatar"
            src={user.data.avatar} />
        </div>
        <div className="content-container">
          <h4 className="list-group-item-heading title">
            {user.data.nickname}
          </h4>
          <h4 className="list-group-item-heading timestamp">
            {'- ' + timestamp}
          </h4>
          <p className="list-group-item-text text">
            {tweet.data.text}
          </p>
          <div className="tweet-actions">
            <EditLink tweet={tweet} />
          </div>
        </div>
      </li>
    );
  }

}

Tweet.propTypes = {
  tweet: PropTypes.object.isRequired,
  user: PropTypes.object.isRequired
};

export default connect(function(getState, props) {
  const tweet = props.tweet;

  return {
    user: getState('user.byId', {
      id: tweet.data.user
    })
  };
})(Tweet);
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { connect } from 'lore-hook-connect';
import EditLink from './EditLink';

@connect(function(getState, props) {
  const tweet = props.tweet;

  return {
    user: getState('user.byId', {
      id: tweet.data.user
    })
  };
})
class Tweet extends React.Component {

  static propTypes = {
    tweet: PropTypes.object.isRequired,
    user: PropTypes.object.isRequired
  };

  render() {
    const { tweet, user } = this.props;
    const timestamp = moment(tweet.data.createdAt).fromNow().split(' ago')[0];

    return (
      <li className="list-group-item tweet">
        <div className="image-container">
          <img
            className="img-circle avatar"
            src={user.data.avatar} />
        </div>
        <div className="content-container">
          <h4 className="list-group-item-heading title">
            {user.data.nickname}
          </h4>
          <h4 className="list-group-item-heading timestamp">
            {'- ' + timestamp}
          </h4>
          <p className="list-group-item-text text">
            {tweet.data.text}
          </p>
          <div className="tweet-actions">
            <EditLink tweet={tweet} />
          </div>
        </div>
      </li>
    );
  }

}

export default Tweet;

src/models/tweet.js

const fields = {
  data: {
    text: ''
  },
  validators: {
    text: [function(value) {
      if (!value) {
        return 'This field is required';
      }
    }]
  },
  fields: [
    {
      key: 'text',
      type: 'text',
      props: {
        label: 'Message',
        placeholder: "What's happening?"
      }
    }
  ]
};

export default {
  dialogs: {
    create: fields,
    update: fields
  }
}

Next Steps

Next we're going to create a way to delete tweets.