Skip to content

Instantly share code, notes, and snippets.

@idlefingers
Created October 16, 2015 14:37
Show Gist options
  • Save idlefingers/bb0bf85af3b57aa23d20 to your computer and use it in GitHub Desktop.
Save idlefingers/bb0bf85af3b57aa23d20 to your computer and use it in GitHub Desktop.
Tjejer Kodar Todo App Guide

Part 1: Creating the project

In this part of the guide we will setup the basic project structure, with an empty HTML document and configure babel to compile our JSX templates.

Installing prerequisites

We need to install nodejs and babel so we can compile our JSX (React) code. If you have already installed nodejs, you can skip this section!

For Mac users:

  • Go to https://nodejs.org/en/ and download & run the installer
  • Open the Terminal (found in /Applications/Utilities)
  • Run sudo npm install --global babel
  • Type admin password when prompted.

For windows users:

  • Go to https://nodejs.org/en/ and download & run the installer
  • Open the Command Prompt
  • Run npm install --global babel

To check it all worked fine, for both Windows or Mac, you can type babel --version in the Terminal or Command Prompt. If you see something along the lines of "5.8.23 (babel-core 5.8.25)", it worked.

Project structure

It is common practice for JavaScript applications to put their code in a folder called src, which will then get compiled into another folder called build. We'll use a program called Babel to do this compiling for us, which means, once set up, we won't need to worry about the build folder at all.

Create a new folder called todo-app to contain the project. Within this folder, make a folder called vendor and another one called src. We'll put external JavaScript frameworks in vendor, and we'll write our own code in src. This keeps things nice and separated.

Next, we'll add the React framework file. You can find the framework on the USB drive in /files/react.js. Copy it into your project's vendor folder.

Next, let's setup our blank HTML document. Create a file called index.html directly in the todo-app folder. This is the file which will load all the JavaScript files we write, and will be where the app all comes together. Write the following in index.html to load the react.js file we copied over and set up an empty <div> element which we can later load our application into:

<!DOCTYPE html>
<html>
  <head>
    <title>Todo App</title>
    <meta charset="utf-8">
  </head>
  <body>
    <div id="app"></div>
    <script src="vendor/react.js"></script>
  </body>
</html>

Hello world

Now we have the basic structure setup, let's write our first bit of JavaScript. Before we go and start using React, lets make sure we can run the app with Babel and have our JavaScript compile.

In the src folder, create a file called app.js. This will be the entry point of our application later, but for now, let's just write something which we can immedediately see works when we load the page. In JavaScript, you can trigger a popup on the page using the alert() function, so lets just use that to say "Hello world!". In app.js, add the following:

alert("Hello world!");

Finally, let's include this new file in our index.html document to have it run when we load the page. Below the <script> tag where we import React, add:

<script src="build/app.js"></script>

Notice we're including the file from the build folder instead of src. Currently that file doesn't exist, but once we start compiling our JavaScript, it'll be created for us by Babel.

Building and testing

Now we have everything setup, lets build our JavaScript and open our app in Chrome. To build the JavaScript, in your Terminal (or Command Prompt for Windows users), run the following:

babel src --watch --out-dir build

This tells Babel to watch for any changes made within src and automatically compile them into build. You can leave this running while you continue with the rest of the tutorial.

Now we can open our app in Chrome! Switch to Chrome and choose File > Open. Navigate to your index.html file. When it loads, you should see the "Hello world!" alert popup!

Style

We're not going to cover CSS in this tutorial, if you're familiar with CSS feel free to style the todo list as you'd like! Or, if you'd like to hit the ground running, we've included a premade stylesheet on the USB stick under files/style.css. If you'd like to use it, copy it into the base project folder, and then update the <head> of index.html to link to the stylesheet, like so:

<head>
  <title>Todo App</title>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

Part 2: Our first components

In this part of the guide, we'll create our first two React components. One for the list, and one for each item in the list.

The "App" Component

Let's replace the "Hello world" code in app.js with the bare minimum needed to make a React component which renders something to the DOM.

var TodoApp = React.createClass({

  render: function () {
    return (
      <div>
        <ul>
          <li>This will be a todo item!</li>
        </ul>
      </div>
    );
  }

});

ReactDOM.render(<TodoApp />, document.getElementById('app'));

This creates a new React component (or class) called TodoApp, and gives it a render function which will just return static HTML, and then tells . If you reload the page in Chrome, you should now see your list with one item in generated by react.

The "Item" component

Next, let's create the component which will be used to render each todo "item". Let's create a file for our new component called item.js in the src folder.

On to building the Item component. For now, lets build a simple component which uses a "text" property and renders an <li> element with the text in.

var Item = React.createClass({

  render: function () {
    return (
      <li>
        <span className="text">{this.props.text}</span>
      </li>
    );
  }

});

Now, we just need to include our new item.js file in our HTML document, and then update app.js to use it. In index.html, above the line where we include app.js, add the following line to include the item.js component.

<script src="build/item.js"></script>

Finally, let's update app.js to use the new Item component. Replace the line in render() which builds an <li> with the JSX code to use the new Item component we just created. In it, we can give it the title for the item hard-coded for now.

render: function () {
  return (
    <div>
      <ul>
        <Item text="This is a todo item!" />
      </ul>
    </div>
  );
}

Reload the page in Chrome and you should now see the todo item rendered via the Item component!

Part 3: Using an array of objects for Todo Items

So now we have the basic structure set up, we're ready to start building our Todo items more dynamically. Soon we'll get on to building a form so that we can enter our items on the page and display them, but for now, let's make the App continue to use hard-coded values but start moving it in the direction to be able to handle the data which would come from the form.

Todo item data

Our Todo items are really quite simple; they have a text, and they can either be completed or not. So, lets store Todos as Objects with those keys. Let's put them into the TodoApp state using the built-in React function getInitialState(). In app.js, let's implement this function:

getInitialState: function () {
  return {
    items: [
      { text: "This is a todo item", complete: false },
      { text: "This is another item", complete: false }
    ]
  };
},

We could iterate through these items directly in the render() function in the TodoApp component, but it's much nicer and cleaner to create a new function which will build it. In app.js, let's build a new item called buildItemNode(). It'll take two arguments; the item object, and the index of the item in the array of items:

buildItemNode: function(item, index) {
  return (
    <Item
      text={item.text}
      complete={item.complete} />
  );
},

Then, we can use this in the JSX in the render() function using {this.state.items.map(this.buildItemNode)}. app.js should now look like this:

var TodoApp = React.createClass({

  getInitialState: function () {
    return {
      items: [
        { text: "This is a todo item", complete: false },
        { text: "This is another item", complete: false }
      ]
    };
  },

  buildItemNode: function(item, index) {
    return (
      <Item
        text={item.text}
        complete={item.complete} />
    );
  },

  render: function () {
    return (
      <div>
        <ul>
          {this.state.items.map(this.buildItemNode)}
        </ul>
      </div>
    );
  }

});

ReactDOM.render(<TodoApp />, document.getElementById('app'));

In Chrome, if you reload, you should now see your list of Todo items populated by the array of objects!

Mapping arrays and React 'keys', oh my!

If you have the web inspector open in Chrome while you've been developing your app (which is a good thing to do!), you may have noticed that now we're iterating on an array of objects when building the app, we suddenly start seeing a warning from React about unique keys:

React keys warning

This happens because React wants to add a unique identifier to each item when it knows you're iterating over a list. We need to choose what the key should be so that, if we wanted to later, we could reliably use it to refer to that item in the component.

When we are iterating over the array of todo items and building the Item components, the .map() function we are using gives us the item which is currently in the iteration, but also the position in the array of that item. We can use that position as the key value since we know it will be unique.

Let's update the JSX in the buildItemNode() function to include the key:

buildItemNode: function(item, index) {
  return (
    <Item
      key={index}
      text={item.text}
      complete={item.complete} />
  );
},

Reload the page and the error will be gone!

Part 4: A Form Component

Our app is beginning to take shape. We have the ability to render an array of items into our list, but the list is currently hard coded. We can build a Form component which we can use to populate our items array.

The basic component

Let's start by setting up a new component. As before, we'll need to create a new file in src and we'll need to include it in index.html. Let's call it form.js. We can include it just above the app.js in index.html:

<script src="build/form.js"></script>

For now, lets just make the component render a form so we can see it in the app, then we'll handle form submission later. Here's our minimal form component:

var Form = React.createClass({

  render: function() {
    return (
      <form>
        <input />
        <button>Add item</button>
      </form>
    );
  }

});

Then, in app.js, let's include this Form component in the render() function:

return (
  <div>
    <Form />
    <ul>
      {this.state.items.map(this.buildItemNode)}
    </ul>
  </div>
);

Reload the page in Chrome and you now have a very basic, minimal form! Yay!

Handling form submission and passing it to the app

When someone submits the form with a new item, we need to capture the form submission, and get the value of the text field. Then, since we're storing the todo items in the TodoApp component, we need a way of passing that new item back up to the TodoApp component from the Form component.

First, let's catch the form submission. In JSX, you can catch the form submission by adding an onSubmit attribute to the <form> tag in the render() function. In that attribute, we will reference a new function which we'll write on the Form component. To begin with, so that we can see the component is working, we'll log the value of the text field to the console.

In form.js, let's update the render() function to add the onSubmit handler:

render: function() {
  return (
    <form onSubmit={this.handleSubmit}>
      <input ref="text" />
      <button>Add item</button>
    </form>
  );
}

So now, when the form is submittied, it will trigger the handleSubmit() function with the 'submit' event, but the browser will also continue to trigger the default behaviour of the event, which in the case of a form will cause the page to reload. We need to prevent this reload so that we can use the data in the form and manipulate our application.

Let's build the handleSubmit() function so we can get the value of the text field and log it. Above the render function, let's define this new handleSubmit() function. The function takes a JavaScript event as an argument. We can prevent the default form event from happening by calling the preventDefault method on the event object.

handleSubmit: function(event) {
  event.preventDefault();
},

So now we get the submit event, we prevent the browser from navigating away from the page, now we just need to retrieve the value of the text field. You may have noticed we added a ref attribute to the input tag when building the form html. This ref attribute allows us to very easily select the element using React using the this.refs object. Let's add that the handleSubmit() and log the value to the console.

handleSubmit: function(event) {
  event.preventDefault();
  var textNode = ReactDOM.findDOMNode(this.refs.text);
  console.log(textNode.value);
},

Now if you reload the page in Chrome, type something in the text field and click submit, you'll see the value of the text field in the web inspector console.

Handing the new item in the app

We need to do a couple of final tweaks to our Form and TodoApp components to get the Form to pass the input data to the TodoApp, and the TodoApp to update its state with the new data. Lets start with getting the data from the Form to the TodoApp. We'll do this by passing a callback function over to the form when we build it in the TodoApp JSX. This will allow us to call a function on the TodoApp from the Form component without the Form component really knowing what that function is or does.

In app.js, lets create a new function which will handle a new item object and update the component's state to include the new object. Let's call it handleNewItem() and make it expect an item object as an argument:

handleNewItem: function(item) {
  var newItems = this.state.items.concat([item]);
  this.setState({ items: newItems });
},

Let's go through exactly what this function does. First, it takes the current array of items which is in this.state (which, when the page is reloaded would be whatever is in the getInitialState() function), and it then adds the new item object to the array using the concat method which is built in to JavaScript arrays. Then, it updates the TodoApp's state to be this new state which is a combination of the old array, with a new item at the end. React will see this state update and will automatically re-render it's content to reflect the new state.

Now we just need to make the form call this function to add the new item. This is where props come in to play again. So far we've only used them for string values (like this.props.text on the Item component), but they can actually be used for anything. In this case, we'll use them to pass a reference to this new handleNewItem() function so that the Form can pass its new item in.

In app.js, add onItemAdded={this.handleNewItem} to the Form component. So the render() function should now look like this:

render: function() {
  return (
    <div>
      <Form onItemAdded={this.handleNewItem} />
      <ul>
        {this.state.items.map(this.buildItemNode)}
      </ul>
    </div>
  );
}

Finally, in form.js, we can call this prop to add the item to the items array and trigger the state change. Let's change handleSubmit() to use the onItemAdded() function which was passed in:

handleSubmit: function(event) {
  event.preventDefault();
  var textNode = ReactDOM.findDOMNode(this.refs.text);
  console.log(textNode.value);
  this.props.onItemAdded({ text: textNode.value });
},

We can add one more little improvement to the handleSubmit() function while we're in here. Let's clear the input field once the new item is added so that it can immediately be used to add another item! In handleSubmit(), at the bottom of the function, add textNode.value = "" to clear it.

Part 5: Completing todos

Our app now lists todos, and makes it possible to add new ones to the list. Cool! Now we should make it possible to complete them! When a todo is complete, we'll add a "complete" css class to the element so that it can be styled to look different from the other items in the list.

Once again, callbacks to the rescue

When an Item is completed, really, we're just updating an Item in the state of the TodoApp. We'll use a similar approach as we did in the handleNewItem() function where we pass a callback function into the component as a prop value, but this time it will need to pass over a little more information because we need to be able to reference which item is actually being updated.

Let's start by defining the function which we will use as a callback on the TodoApp. Let's call it updateItem() and build it to expect the item's index, and the action we're going to perform on the item (updating the completion, in this case).

In app.js, create this new function which will take the index of the item we're updating, find it in the current state, change it, and then use it to set the new state on the app.

updateItem: function(index, action) {
  var items = this.state.items;
  items[index].complete = action.complete;
  this.setState({ items: items });
},

Now we need to tell the Item that this function should be called to update the TodoApp state when the item is changed. Let's add it to the buildItemNode() function in app.js:

buildItemNode: function(item, index) {
  return (
    <Item
      key={index}
      text={item.text}
      complete={item.complete}
      onUpdate={this.updateItem} />
  );
},

That looks good, but unfortunately, it's not this simple. In the last example when we used a callback function passed as a prop (for adding a new item), we didn't need any supporting information, but this time we need to pass along the index of the item so the updateItem() function knows what's going on. It would be really nice it we could just say onUpdate={this.updateItem(index)}, but that would cause the function to be called - we just want to pass the function onwards and not call it until the Item wants to.

Using bind to 'magically' keep arguments for later

So we need a way of saying "When this function is called, remember the index of the item, but let's not call it yet". This actually is quite a pain in JavaScript due to the whole this binding issue which we tried to briefly explain in the introduction to JavaScript, but also because of the nature of passing around functions as arguments. We need to pass the updateItem() function anonymously to the Item, keeping the index. This can be done using a function called .bind():

buildItemNode: function(item, index) {
  return (
    <Item
      key={index}
      text={item.text}
      complete={item.complete}
      onUpdate={this.updateItem.bind(this, index)} />
  );
},

Here, we used this new .bind() function to pass through the index of the item.

Don't worry if you're not really following what's gone on here - it's one of the most difficult parts of JavaScript, I find, to use, understand, or explain! So please forgive my probably quite bad attempt at explaining!

Toggling Item completion

Now we have the Item in the TodoApp passing through this updateItem() function, let's update the Item component to use it! In item.js, let's define a new function which will be called when an Item is clicked. For now, let's just make the function log to the console the current completion state of the Item:

var Item = React.createClass({

  toggleComplete: function () {
    if (this.props.complete) {
      console.log("This item is complete! We should mark it as not complete now..");
    } else {
      console.log("This item is not complete! We should mark it as complete now..");
    }
  },

  render: function () {
    return (
      <li onClick={this.toggleComplete}>
        <span className="text">{this.props.text}</span>
      </li>
    );
  }

});

Now if you reload the page in Chrome, you should see it tells you the completion state of your items. Try updating one of the items in the getInitialState() function in app.js to make it complete, and then try clicking on it!

Cool. So now lets switch this to actually updating the TodoApp using the updateItem() function we hooked up earlier. In item.js, let's rewrite the toggleComplete() function:

toggleComplete: fucntion () {
  if (this.props.complete) {
    this.props.onUpdate({ complete: false });
  } else {
    this.props.onUpdate({ complete: true });
  }
},

To check that it's working, we can add a console.log() line to app.js in the updateItem() function to just log the item which has been changed:

updateItem: function(index, action) {
  var items = this.state.items;
  items[index].complete = action.complete;
  console.log(items[index]);
  this.setState({ items: items });
},

Awesome. Reload the page and you should see the item toggle it's complete state each time you click it. That's cool, but it's not very visual! Let's use the Item's complete state to add a CSS class to it so we can make it look different when it is complete. Then we can really see it's working! In item.js, let's add a little bit to the render() function:

render: function () {
  var complete = this.props.complete ? "complete" : "not-complete";

  return (
    <li onClick={this.toggleComplete} className={complete}>
      <span className="text">{this.props.text}</span>
    </li>
  );
}

Here, we're using a "ternary" operator to set the complete variable to "complete", or "not-complete". Ternery operators are useful for very short conditions. You can read more about ternary operators here. We then use this string to add a class to the <li> node.

Reload the page, and if you're using the stylesheet we provided, you should see a nice big check mark on completed items! Yay!

Part 6: Removing items

Our app is really starting to take shape and look good now. We can add items, we can mark them as done, but since we can't remove them, it makes a typo in a new item become a long-lasting thing! Let's update our app to make each item have a 'x', which, when clicked, removes that item from the TodoApp state object.

X marks the spot

Let's start by adding the 'x' to the Item component and writing a remove() function on the Item to deal with what should happen when the x is clicked. In item.js, add the link to the JSX in the render() function:

render: function () {
  var complete = this.props.complete ? "complete" : "not-complete";

  return (
    <li onClick={this.toggleComplete} className={complete}>
      <span className="text">{this.props.text}</span>
      <a href className="remove" onClick={this.remove}>×</a>
    </li>
  );
}

Then, let's write the remove() function in item.js. We're going to make use of the updateItem() function on the TodoApp which we're already passing into the Item to handle removal, just to keep things simple. We're also going to need to prevent a couple of default behaviours on the event object. I'll explain more on that in a minute.

remove: function(event) {
  event.preventDefault();
  event.stopPropagation();
  this.props.onUpdate({ remove: true });
},

First, this function prevents the default behavior of the <a> tag which we're using. By default an <a> tag would create a link which would cause the browser to navigate away from the page. We want to prevent that!

Then, we're using a new function on the event object, .stopPropagation(). This is to handle another default behavior in the way that interaction events are triggered in browsers. We've already bound on to the 'click' event on the <li> tag which contains the entire Item which we used in the last part of this tutorial to toggle Item completion. Now we've added a remove link on top, but the click event on the link will 'propagate' up the DOM to the <li> element (and continue on up, actually). Since that would cause a strange behavior in our app (clicking remove would toggle an Item's completion before removing it), we'll stop that behavior!

Finally, we're using the onUpdate() function and in the action object (which is the updateItem() function in the TodoApp component), telling the function we want to remove this item.

Removing the item from the list

The Item now properly handles the click on it's remove button, but the updateItem() function in the TodoApp component needs updating to handle this new responsibility.

Currently, the updateItem() function accepts an action object, which in the previous part of the guide we used to pass the { complete: true } or { complete: false } in. Now we're also using { remove: true }, so in the onUpdate() function, we should check which one we're dealing with first, and then handle the request appropriately. In app.js, update the updateItem() function to look like this:

updateItem: function(index, action) {
  var items = this.state.items;

  if (action.remove) {
    items.splice(index, 1);
  } else {
    items[index].complete = action.complete;
  }

  console.log(items[index]);
  this.setState({ items: items });
},

So this checks the action now, and if it includes a remove key, we'll remove the item from the array using the .splice() function which is built in to JavaScript arrays. The .splice() function takes the index of the item you want to start removing at, and then how many items you want to remove. We only ever want to remove one item; the item which is at the current index!

Reload the page in Chrome and you should see an 'x' next to each item! When you click it, the item should be removed! If you're using our stylesheet, the x only shows when you hover over the Item.

Part 7: Local Storage

We now have a very functional Todo application which allows us to add, remove, and complete todos. The only problem now is that when we reload the page, we always lose any new todos we've added. Obviously, that's no good!

For persisting data in apps like this, developers are faced with two options; use an external service to store the data and then read and write changes to that service via an API, or persist the items in the browser. Most commonly people opt for an API because that means that you can be sure the data is saved for a long time, and you don't need to worry if you change browser, or switch computers.

Using an API is definitely where we would go with our app in the future, given more time to work on it, but for now, localStorage is a quick and simple solution to the problem which at least means we can make changes to the code and reload the page without losing our changes.

How localStorage works

localStorage is a very simple key/value store which modern browsers have built in. It is very limited because it only allows you to store a string or integer as a value, so we'll need to get sneaky when storing our list of items. First, let's just have a little look at localStorage. In Chrome, open up the web inspector, and type localStorage in the console. It should return an empty object. Let's experiment with adding and modifying items in localStorage, in the console:

localStorage.fruitName = "Orange"; // stores a new 'fruitName' key with the value "Orange".
localStorage.fruitName = "Pear"; // changes the 'fruitName' to "Pear".
localStorage.removeItem("fruitName");

If you reload the page at any point, you'll see that whatever you put in localStorage is still there.

That's great, but our list of todo Items is an array of objects which look something like this:

var items = [
  {
    text: "Buy apples",
    complete: true
  },
  {
    text: "Eat apples",
    complete: false
  }
];

In order to store this as a string, we're going to need to serialize the array. Luckily, this is very easy in JavaScript. We can serialize it as json:

var jsonStringOfItems = JSON.stringify(items);

This will give us a string which looks like this: "[{"text":"Buy apples","complete":true},{"text":"Eat apples","complete":false}]". Perfect.

Finally, to get our string back as an array of objects, we can deserialize, like this:

JSON.parse(jsonStringOfItems);

Updating our app

All the changes we need to make to our app to convert it to using localStorage will be done in app.js since we decided to store all state in the TodoApp component.

First, let's add a convenience method to fetch items from our localStorage string. Because we don't want to have to worry about deserializing the string all the time, this will be our only way of interacting with fetching the items elsewhere in our code.

getItemsFromLocalStore: function() {
  return JSON.parse(localStorage.items);
},

Next, we should define a nice way of writing the state object to localStorage whenever it changes. Since we cause the change of state from a couple of different points in the code, it would be nice if we can make this serialization automatically and not have to explicitly call it.

Luckily, React has a built-in function which is perfect for this use case! Each time the state has been changed and React is about to re-render the component, it calls a function called componentWillUpdate() with the props object and state objects which are going to be used to render the refreshed version of the component. Let's implement this function:

componentWillUpdate: function(nextProps, nextState) {
  localStorage.items = JSON.stringify(nextState.items);
},

At this point, if you open Chrome and reload the page, when you add a new Item, toggle an Item's completion status, or remove an Item, if you inspect localStorage in the web inspector console, you'll see that your items are now being updated! Excellent!

Now we just have one last thing to handle - when we reload the page, we're still setting a predefined list of Items in the getInitialState() function. We just need to update that to use localStorage instead of a hard-coded list of items!...

getInitialState: function() {
  return {
    items: this.getItemsFromLocalStore()
  }
},

Yay! Page reloads now show the same list of todo items! We have persistence! Good job!

One last thing

There's a little bug we introduced here which we wouldn't see straight away. When you first visit the page, with no items key in your localStorage, when React runs the getInitialState() function and then getItemsFromLocalStore(), the localStorage.items key will not exist yet, and so we'll see an error when we try to deserialize null.

Let's fix this by making sure, when the app initially loads, that we have something in localStorage.items. In app.js, let's use another core React function called componentDidMount() which gets run just as this compenent is being rendered into the DOM...

componentDidMount: function() {
  if (localStorage.items == null) {
    localStorage.items = JSON.stringify([]);
  }
},

This just create a JSON string from an empty array ([]) and stores it in localStorage if the items key has no value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment