DEV Community

Cover image for #Todo app tutorial in Ember Octane for those who know React
JennyJudova (she/they)
JennyJudova (she/they)

Posted on

#Todo app tutorial in Ember Octane for those who know React

Like many Ember developers I came across it when I started working in my current job.

Ember has been around since 2011 (React was released in 2013) and is used by some well known websites like Linkedin, Apple Music and Twitch. Unlike Vue, Gatsby or React Ember, never was the sexy new toy every developer spent the weekend learning. Which I think is a shame as the framework is scalable and I find that it offers itself to collaboration far better than some other frameworks.

So to encourage others to explore Ember here is a tutorial of how to build a TODO app with CRUD functionality.

Why Am I comparing Ember and React?

React is the lingua franca of front end development. And, I find that, it’s always easier to learn a new language when it's directly compared with something you already know, even if the comparisons are not perfect.

Bellow is my take on Ember and its community but feel free to skip to the tutorial below that.

React vs Ember

React is not a fussy framework you throw everything into one component: the DOM, a few functions, an API request, and the kitchen sink. And everything works. No matter how messy you are, React will figure it out. 
Ember likes tidiness. Ember likes order. In Ember everything has its place. One cannot be messy in Ember. 
React is awesome for solo weekend projects, Ember is great if you are working with others. 

Quirks anyone new to Ember will face:

Lack of resources 

Google “How to do … in React?’ and you will get thousands of resources from official documentation, to Stack Overflow, to opinion pieces on Medium and educational ones on Dev.to. Learning React I can't recall many cases where a Google search would fail to answer my question. 
Ember has fewer resources. There is the official documentation (which is amazing), a few blogs, and the Ember discord channel. This is a great list of Ember resources.

It’s hard to stay anonymous

As React is known and used by so many developers I sometimes question if it can even be called a community. Ember is used by fewer developers and almost all developers that actively use it can be found on the Ember discord forum. The main event of the year is Ember Conf, and that gathers most of Ember devs. Another thing to flag up is that most developers who use Ember on a day-to-day basis work for a few dozen companies. Because of the size and the fact that the community is so centralised its hard to stay anonymous within it. 

Community made of professionals

I have yet to meet developers who took up Ember as a fun weekend thing to learn. So in most cases we all learn Ember because companies we work for use Ember. Ember is a community of professionals, which also makes it interesting because once you get past the initial intimidation, everyone within it is your current, past, or future colleague. 

Tutorial

As a reminder I am a newbie to Ember teaching others so this tutorial is very much a practical how to get stuff to work here and now rather than a birds eye view of how things work in Ember. For documentation, check out https://emberjs.com/.

Getting started.

Open your terminal

  • 1 - Run npm install -g ember-cli

The version I am using now is

ember --version
ember-cli: 3.16.0
node: 12.16.0
os: linux x64
Enter fullscreen mode Exit fullscreen mode
  • 2 - ember new todo-app
  • 3 - cd todo-app
  • 4 - open the app in your editor
  • 5 - back in the terminal run ember serve or npm start This will start the app and you can view it in http://localhost:4200/

The first thing to flag up is the folder and file structure in Ember vs that of React.
The most basic app in React will have

index.html 
style.css
index.js
Enter fullscreen mode Exit fullscreen mode

You can throw everything into index.js (functions, api calls etc) never touching the html and the css files and it will work.

In ember every new app will have:

App
    Components
    Controllers
    Helpers
    Models
    Routes
Styles
Templates
    Application.hbs
App.js
Index.html
Router.js
Enter fullscreen mode Exit fullscreen mode

To get ‘Hello World’ printed on the screen go to application.hbs delete

{{outlet}}

and paste in

<h1>Hello World</h1>

now for our app change it back to

{{outlet}}

Everything that would find its way into one component in React will be scattered between Route, Component and Template in Ember.

Template is your html. Hbs stands for handlebar. The main thing to know is that Handlebars are logic less so no mapping or filtering within your html.

Route... the best way of thinking about it is that Route is your Component Will Mount (it's not really but for practical purposes of getting s*it done think of it that way). The data that you want to see on the screen when the page loads is fetched/axioed/requested in the Route.

Component is where you put any functions that will react to any user input, button clicks, essentially any user interactions.

Helpers is where little reusable helper functions go. For example if you are converting Fahrenheit into Celsius this is where the function that does that goes.

As for Controllers well to quote a question posed n Ember Guide ‘Should we use controllers in my application? I've heard they're going away!’

Step 1 - Creating a route

In terminal type

ember g route todo

The output in your terminal will be:

installing route
  create app/routes/todo.js
  create app/templates/todo.hbs
updating router
  add route todo
installing route-test
  create tests/unit/routes/todo-test.js
Enter fullscreen mode Exit fullscreen mode

Step 2 - Displaying the todo list in console

Let's start by adding adding some existing to do's.
Go to app/routes/todo.js the template should look something like:

import Route from '@ember/routing/route';

export default class TodoRoute extends Route {
}
Enter fullscreen mode Exit fullscreen mode

To add the todo dataset add the model to the route:

import Route from "@ember/routing/route";

export default class TodoRoute extends Route {
  model() {
    return [
      {
        id: 1,
        todo: "todo 1"
      },
      {
        id: 2,
        todo: "todo 2"
      },
      {
        id: 3,
        todo: "todo 3"
      }
    ];
  }
}
Enter fullscreen mode Exit fullscreen mode

Now go to app/templates/todo.hbs delete whatever is in it and add:

<h1>TODO app</h1>
{{log this.model}}
Enter fullscreen mode Exit fullscreen mode

In your terminal run ember serve

Open your web browser go to http://localhost:4200/todo you should see 'TODO app' on the page. Open the Inspector -> Console. In the console you should see your model array.

Step 3 - Displaying the todo list on the screen

So here I make a decision that I will build the whole app in one component. Feel free to refactor it to be in separate components. I would argue that the 'list of todos' and the ''add new todo button' should be two separate components but I will let you figure out how to refactor this.

Part 3.1

In Terminal run:
ember g component todo-app

You will see the following in your terminal

installing component
  create app/components/todo-app.hbs
  skip app/components/todo-app.js
  tip to add a class, run `ember generate component-class todo-app`
installing component-test
Enter fullscreen mode Exit fullscreen mode

Go ahead and follow the 'tip' and run the command ember generate component-class todo-app.

Now if you go to app/components you will find todo-app.hbs and todo-app.js.

Todo-app.hbs is your html, and todo-app.js is your logic and action handling part.

Let's go to todo-app.hbs delete whatever is there and add

<p>sanity check</p>

If you now go to http://localhost:4200/todo or http://localhost:4200/ you will not see sanity check on the screen.

To get todo-app.hbs displaying on the screen go to todo.hbs and add to the file so that you have

<h1>TODO app</h1>
<TodoApp />
{{log this.model}}
Enter fullscreen mode Exit fullscreen mode

Now go to http://localhost:4200/todo - viola! Sanity check is displaying.

Part 3.2

So if you go to todo.hbs and take out

{{log this.model}}

and go to todo-app.hbs and add it there

<p>sanity check</p>
{{log this.model}}
Enter fullscreen mode Exit fullscreen mode

You will now get undefined in your console rather than the model.

So let's pass the model from todo to the component todo-app by changing

<TodoApp />

to

<TodoApp @model={{this.model}}/>

Despite this change you will still get undefined because this.model was passed as @model to the component.

So lets change

{{log this.model}}

to

{{log @model}}

Viola! You are back to square 0 with the model showing in console. Now let's actually display it.

Part 3.3

In React the most basic solution to displaying 3 todos would just be:

<ul>
    <li>{this.state.model[0].todo}</li>
    <li>{this.state.model[1].todo}</li>
    <li>{this.state.model[2].todo}</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

You can try writing something along these lines in todo-app.hbs but this will not work. So another option of displaying this in React is by using .map.

So something like this:

<ul>
  {this.state.model.map(todo => {
    return <li key={todo.id}>{todo.todo}</li>;
  })}
</ul>
Enter fullscreen mode Exit fullscreen mode

Templates are logic less and this means that the javascript .map will not work however templates have helpers that can bring some logic to the template.

We will be doing something similar to .map by using a helper 'each'.

So let's go to todo-app.hbs and add:

<ul>
  {{#each @model as |item|}}
  <li>{{item.todo}}</li>
  {{/each}}
</ul>
Enter fullscreen mode Exit fullscreen mode

Nice! The todo list is displaying.

Step 4 - Adding actions

Now lets add a textfield and a button so that you can add new todos to the list.

On the html side the tags will be identical to the ones you would have used in a React component. So lets add this to todo-app.hbs:

<ul>
  {{#each @model as |item|}}
  <li>{{item.todo}}</li>
  {{/each}}
</ul>
<form>
  <input placeholder='Add todo' type='text' />
  <button type='submit'>Add</button>
</form>
Enter fullscreen mode Exit fullscreen mode

This will display an input field and a button but of course this will not do anything, so it's finally time to look at todo-app.js. But before we do this let's see how this would have looked in React.

React view

import ReactDOM from "react-dom";
import React, { Component } from "react";

class Todo extends Component {
  constructor(props) {
    super(props);
    this.state = {
      model: [
        {
          id: 1,
          todo: "todo 1"
        },
        {
          id: 2,
          todo: "todo 2"
        },
        {
          id: 3,
          todo: "todo 3"
        }
      ],
      text: ""
    };
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleChange = this.handleChange.bind(this);
  }

  handleSubmit(e) {
    e.preventDefault();
    const i = this.state.model[this.state.model.length - 1].id + 1;
    let newTodo = {
      todo: this.state.text,
      id: i
    };
    this.setState(prevState => ({
      model: prevState.model.concat(newTodo),
      text: ""
    }));
  }

  handleChange(e) {
    this.setState({
      text: e.target.value
    });
  }

  render() {
    return (
      <div>
        <h1>TODO LIST</h1>
        <ul>
          {this.state.model.map(todo => {
            return <li key={todo.id}>{todo.todo}</li>;
          })}
        </ul>
        <form onSubmit={this.handleSubmit}>
          <input value={this.state.text} onChange={e => this.handleChange(e)} />
          <button>Add</button>
        </form>
      </div>
    );
  }
}

ReactDOM.render(<Todo />, document.getElementById("root"));
Enter fullscreen mode Exit fullscreen mode

Back to Ember

Now lets write handleChange and handleSubmit in Ember.

The React

  handleChange(e) {
    this.setState({
      text: e.target.value
    });
  }
Enter fullscreen mode Exit fullscreen mode

barely changes as it becomes:

  @tracked
  text = "";

  @action
  onChange(e) {
    this.text = e.target.value;
  }
Enter fullscreen mode Exit fullscreen mode

@tracked is your state but it's best you read about @tracker and @action in the Ember guide.

And handleSubmit goes from:

  handleSubmit(e) {
    e.preventDefault();
    const i = this.state.model[this.state.model.length - 1].id + 1;
    let newTodo = {
      todo: this.state.text,
      id: i
    };
    this.setState(prevState => ({
      model: prevState.model.concat(newTodo),
      text: ""
    }));
  }
Enter fullscreen mode Exit fullscreen mode

to:

  @action
  submit(model, e) {
    e.preventDefault();
    const i = model[model.length - 1].id + 1;
    const newTodo = {
      id: i,
      todo: this.text
    };
    model.pushObject(newTodo);
  }
Enter fullscreen mode Exit fullscreen mode

So todo-app.js ends up looking like this:

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";

export default class TodoAppComponent extends Component {
  @tracked
  text = "";

  @action
  submit(model, event) {
    event.preventDefault();
    const i = model[model.length - 1].id + 1;
    const newTodo = {
      id: i,
      todo: this.text
    };
    model.pushObject(newTodo);
    console.log("add", model);
  }

  @action
  onChange(e) {
    this.text = e.target.value;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5 connecting .js with .hbs

React render looks like this:

        <form onSubmit={this.handleSubmit}>
          <input value={this.state.text} onChange={e => 
            this.handleChange(e)} />
          <button>Add</button>
        </form>
Enter fullscreen mode Exit fullscreen mode

in Ember your todo-app.hbs should look like this:

<form onsubmit={{fn this.submit @model}}>
  <input placeholder='Add todo' type='text' value={{this.text.todo}} 
    onchange={{fn this.onChange}} />
  <button type='submit'>Add</button>
</form>
Enter fullscreen mode Exit fullscreen mode

So the full todo-app.hbs looks like this:

<ul>
  {{#each @model as |item|}}
  <li>{{item.todo}}</li>
  {{/each}}
</ul>
<form onsubmit={{fn this.submit @model}}>
  <input placeholder='Add todo' type='text' value={{this.text.todo}} onchange={{fn this.onChange}} />
  <button type='submit'>Add</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Step 6 adding the Delete function

I have to admit delete will not look pretty in React or Ember because we are dealing with an array of objects. If it was an array of strings life would have been easier.

In React it looks like this:

  removeItem(id) {
    let model = this.state.model;
    const index = model
    .map((file, index) => {
      if (file.id === id) {
        return index;
      } else return undefined
    })
    .filter(id => id !== undefined);
    model.splice(index, 1);
    this.setState([...model]);
  }
Enter fullscreen mode Exit fullscreen mode

To a degree Ember follows the same general idea. You figure out the index, then you splice the array. 

There is a but. The HTML side will not re-render if you delete a 'todo’ from the array (I'm new to Ember, so send better solutions). So the work around that I have is: replace the item about to be deleted with an empty object and on the template add 'if empty don’t show’.

todo-app.js

  @action
  delete(model, item) {
    const index = model
      .map((file, index) => {
        if (file.id === item.id) {
          set(item, "id", null);
          set(item, "todo", null);
          return index;
        }
      })
      .filter(id => id !== undefined);
    model.splice(index[0], 1);
  }

Enter fullscreen mode Exit fullscreen mode

and todo-hbs.js

<ul>
  {{#each @model as |item|}}
  {{#if item.id}}
  <li>{{item.todo}}</li>
  <button onclick={{fn this.delete @model item}} type='button'>delete</button>
  {{/if}}
  {{/each}}
</ul>
Enter fullscreen mode Exit fullscreen mode

Again I am new to Ember so any prettier ways of achieving this without using Ember data are welcome.

And with that Viola! The Todo App is done. It is missing editing individual Todo's but I will let you figure that out if you can't here is a the github repo.

And the React to do using a constructor is here the hooks version is here.

Discussion (1)

Collapse
devhammed profile image
Hammed Oyedele

Nice article 👍
But I see some things that you can improve.

  • There is a typo in the React example of delete, it should be this instead:
this.setState({ model: [...model] })
  • The delete method can be shorten to use only filter:
removeItem(id) {
    let updatedModel = this.state.model
        .filter(item => item.id !== id)

    this.setState({ model: updatedModel })
}

This is more concise, I don't know how this translates to Ember but I am guessing it will be a re-assginment like:

@action
delete(model, id) {
   model = model.filter(item => item.id !== id)
}