Get caught up to this step

  • Check out the branch for the previous step: git checkout 11-delete-tasks

Step 12: Edit Tasks

Adding the ability to edit a task

Let's look at our requirements for this feature:

  • Add a edit button that, when clicked on, makes the content of the list item editable.
  • Save changes made to the editable content.
  • Allow for exiting out of the editable state and returning back to the view state.

As before, we'll take a top-down approach, starting with the database.

Database updates

Let's begin by adding a db method for updating the title field of a task:

/both/collections/Tasks.js:

Meteor.methods({
  ...
  '/task/update/title': function(params) {
    let task = Tasks.findOne({_id: params.id });
    Tasks.update(task._id, {
      $set: {
        title: params.title
       }
    });
  },
 ...

});

Tasklist (controller) updates

In our TaskList, we'll add a corresponding front-end handler for updating titles. Also, in our List component, we want to pass in props for enabling editing, and for associating our update handler with the list component.

  handleUpdateTitle(inputValue, id) {
     Meteor.call('/task/update/title', { title: inputValue, id: id }, function(err, result){
       if (err) { console.log('there was an error: ' + err.reason); };
    });
  },
  ...
      <List
      ...
          canEditItem={true}
          handleEditItem={this.handleUpdateTitle}
      ...
        />

List component updates

In the main list component, all we are doing is adding the necessary to props to support this feature, and setting a default value of false.


List = React.createClass({
  //List component API
  propTypes: {
    canEditItem:  React.PropTypes.bool,
    handleEditItem:  React.PropTypes.func,
  },
  getDefaultProps() {
    return {
      ...
      canEditItem:    false,
      ...
    };
  }

We actually have one more update to the list component, but this one is somewhat more significant.

As a component evolves, we are likely to find that we need to sub-divide and refine it into ever smaller and more specific pieces.

Sub-dividing ListItem into ViewListItem and EditListItem.

Since we now want to allow for editing a list item, this means that a list item now will have two states: a viewing state, and an editing state.

For this reason, we'll want to further refine the list item component hierarchy, creating two new child components of ListItem: ViewItem and EditItem

list-item-component

Let's first update ListItem and then add the new child components.

ListItem updates

/client/components/lists/listItem/listItem.jsx:

  getDefaultProps() {
    return {
      editing: false
    };
  },
  getInitialState() {
    return {
      editing:this.props.editing
    };
  },
  toggleEditing() {
    this.setState({editing: !this.state.editing });
  },
  handleUpdates(inputValue){
    this.props.handleUpdateTitle(inputValue, this.props.item._id);
  },
  displayEditableItem() {
    return this.state.editing?
        <EditItem
        content={this.props.item.title}
        handleDone={this.toggleEditing}
        handleUpdates={this.handleUpdates}
        {...this.props} 
      />
      :
      <ViewItem
      ...
        editItem={this.toggleEditing}  
        {...this.props}  
      />
      ;
...

Here, we've added a 'editing' state to ListItem, which we'll use to toggle between viewing and editing an item.

Adding ViewItem

/client/components/lists/listItem/viewItem.jsx:

ViewListItem = React.createClass({
  propTypes: {
    item: React.PropTypes.object.isRequired
  },
  displayTitle(){
    return this.props.isCheckList && this.props.item.done?
        <span className="line-through">{this.props.item.title}</span>
      :
        this.props.item.title
      ;
  },
  displayDeleteBtn(){
    return this.props.canDeleteItem? <span className="pull-right li-option"><IconBtn btnTitle="Delete" btnIcon="glyphicon glyphicon-remove"
      handleClick={this.props.deleteItem} /></span>: null;
  },
  displayEditBtn(){
    return this.props.canEditItem? <span className="pull-right li-option"><IconBtn btnTitle="Edit" btnIcon="glyphicon glyphicon-pencil"
      handleClick={this.props.editItem} /></span>: null;
  },
  displayWithCheckbox(){
     return  <form className="form-inline">
         <div className="checkbox">
           <label>
             <input type="checkbox" checked={this.props.item.done} onChange={this.handleCheckbox} /> {this.displayTitle()}
          </label>
        </div>
      </form>;
  },
  handleCheckbox(e){
    this.props.handleCheckbox(this.props.item);
  },
  displayListItem(){
    return this.props.isCheckList? 
      this.displayWithCheckbox()
      :
        this.displayTitle();
      ;
  },
  render(){
    return <li key={this.props.key} className="list-group-item">
             {this.displayDeleteBtn()}
              {this.displayEditBtn()} 
             {this.displayListItem()} 
           </li>;
  }
});

Here, we've moved our display functions from ListItem into ViewItem. We've also added a handler for displaying and edit button. Since we now have multiple buttons, we also took this opportunity to refactor icon buttons into their own component. Let's add that now:

Adding the IconBtn component

IconBtn

/client/components/buttons/iconBtn.jsx/:

IconBtn = React.createClass({
  propTypes: {
    btnIcon: React.PropTypes.string.isRequired,
    btnTitle:  React.PropTypes.string,
    handleClick: React.PropTypes.func.isRequired
  },
  render() {
    return <button 
        onClick={this.props.handleClick}
        className="btn btn-default btn-xs"
        title={this.props.btnTitle}
        alt={this.props.btnTitle}>
        <span className={this.props.btnIcon} aria-hidden="true"></span>
      </button>;   
  }
});

The purpose of this component is to be used any time have a button with just an icon in it.

Next, let's add what we need for allowing editing of an item:

Adding EditItem

EditItem = React.createClass({
  render(){
    return (
       <li key={this.props.item.key} className="list-group-item">
         <span className="pull-right">
           <button className="btn btn-default btn-xs">Done</button>
         </span>
         <AutoSaveInput {...this.props}  />
       </li>
    ) 
  }
});

While we could have placed the form used for editing of the field, it seems like the need to auto save input will be used elsewhere, so we turned it into a component...

AutoSaveInput

This component calls a parent function with updated data that has been entered. We use a throttle function to ensure we aren't hitting the db too frequently.

AutoSaveInput = React.createClass({
  propTypes: {
    content:        React.PropTypes.string.isRequired,
    handleUpdates:  React.PropTypes.func.isRequired,
    handleDone:     React.PropTypes.func.isRequired,
    autoFocus:      React.PropTypes.bool,
    placeholder:    React.PropTypes.string
  },
  getDefaultProps() {
    return {
      autoFocus: true,
      placeholder: "Edit.."
    };
  },
  getInitialState() {
    return {
      content: this.props.content
    };
  },
  handleOnChange(e){
    const updatedContent = e.target.value;
    const saveInterval = 300;
    this.setState({content: updatedContent});

    this.autoSave = this.autoSave || _.throttle(content => {
      this.props.handleUpdates(content);
    }, saveInterval);

    this.autoSave(updatedContent);

  },

  render() {
    return (

      <form className="form-inline">
       <div className="form-group">
        <input
          className="form-control"
          type="text"
          placeholder={this.props.placeholder}
          value={this.state.content}
          onChange={this.handleOnChange}
          autoFocus={this.props.autoFocus}
          onBlur={this.props.handleDone}
        />
      </div>
      </form>
    )
  }
});

Finally, let's add some spacing between buttons in our list item.

Styling

/client/stylesheets/stylesheet.css:

...
.li-option {
    padding: 0 .25em;
}

You should now be able to click on the edit button and edit a task.

View the completed version of this branch: git checkout 12--edit-task

I hope you've enjoyed this tutorial. To learn about updates to the tutorial (eg adding support for Metor 1.3), follow me at @codechron.

Thanks!