Choo application setup and first elements.

Published on Nov 26, 2016

Other articles in the Choo series.

  1. Choo generalizing your elements.
  2. Choo linting with eslint.
  3. Choo cleaning up home page.
  4. Choo saving data in localStorage.
  5. Choo editing and deleting records.
  6. Choo form validation.
  7. Choo version 5

What's choo

Choo is "another" front end JS framework but takes a "more" functional approach to building UI's than other frameworks out there.

It has a very minimal interface and the learning curve is minimal. After looking at some of the examples in the repo I decided to install it and take it for a spin.

I'm particularly interested on exploring how testable the code will be and how the application will grow (maintainability).

The incredible TV series tracker

The idea is to create a simple application that will allow us to track the Tv shows that we are watching.

Requirements for the first iteration

  • Add a new tv show to the list

For this iteration we will not be adding a back-end.

Getting started

We will start by installing the choo-cli and creating the scafold of our application

Now if we run npm start your default browser should open with the basic application running.

Choo npm start

The project comes configured with budo a development server designed to work with browserify. It provides live-reload and just in time compilation of your modules via browserify.

If we look at the package.json file we will see this:

    "start": "budo ./client.js --live --pushstate --open -- -g es2040"

You will notice that we are asking budo to run a javascript file that acts as the entry point for our application. If you open the client.js you will see this.

  const choo = require('choo')
  const app = choo()

  app.model(require('./models/app'))

  app.router((route) => [
    route('/', require('./pages/home'))
  ])

  const tree = app.start()

  document.body.appendChild(tree)

Budo will generate a plain html file by default but we can tell budo what file to use or simply use a convention and create an index.html file in thefolder where budo runs from.

Here is our html file, we are adding bootstrap to help us here with the initial styling of the application.

  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>Tv series</title>
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css" integrity="sha384-AysaV+vQoT3kOAXZkl02PThvDr8HYKPZhNT5h/CXfBThSRXQ6jW5DO2ekP5ViFdi" crossorigin="anonymous">
    </head>
    <body>

    </body>
    <script src="client.js"></script>
  </html>

Budo will display errors in the browser and the console, clearly indicated, what helps to get things out of the ground until is time to start adding tests.

Budo displays errors in the browser

Budo displays errors in the browser

Budo displays errors in the console

Let's add our first element

We will use the choo-cli to help us generate a new element

The whole idea of choo is that element and pages are just functions that return DOMElements. By default choo uses tagged template strings to build those elements. In this case is using the bel package.

It looks like JSX but is not, they are just strings. You can write your elements as you would any other html in your site and use JavaScript expressions to interpolate or inject other elements.

This hopefully will be clearer later on when we build our shows list.

If you want to refresh on tagged template literal check this article on MDN.

Open your favorite editor and change the content of the element function as shown below:

  function navbar () {
    return html`<nav class="navbar navbar-light bg-faded">
    <button class="navbar-toggler hidden-lg-up" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"></button>
    <div class="collapse navbar-toggleable-md" id="navbarResponsive">
      <a class="navbar-brand" href="#">Tv series</a>
      <ul class="nav navbar-nav">
        <li class="nav-item active">
          <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
        </li>
      </ul>
      <form class="form-inline float-lg-right">
        <input class="form-control" type="text" placeholder="Search">
        <button class="btn btn-outline-success" type="submit">Search</button>
      </form>
    </div>
  </nav>`;
  }

Now we need to add the element to our page.

We will open the /pages/home.js file and we will require the new element and replace the current markup with a call to the NavBar.

  const html = require('choo/html'),
    navBar = require('../elements/navbar')

  module.exports = (state, prev, send) => html`
    <main>
      ${navBar()}
    </main>
  `

Go to your browser and you should see the NavBar rendering properly.

Choo application rendering a Bootstrap nav bar

Adding shows

We could start developing this either from the UI or the model. Since we know how to create elements, let'd start with the UI.

We will use the choo-cli again.

  choo generate element add-show

Change the code inside the navBar function with the following.

  function addShow () {
    return html`
      <div class="row">
        <form>
          <div class="form-group col-xs-5">
            <label for="title" class="col-xs-2">Title</label>
            <div class="col-xs-10">
              <input type="text" class="form-control form-control-sm" name="title" id="title">
            </div>
          </div>
          <div class="form-group col-xs-3">
            <label for="title" class="col-xs-5">Season</label>
            <div class="col-xs-7">
              <input type="number" step="1" min="0" class="form-control form-control-sm" name="season" id="season">
            </div>
          </div>
          <div class="form-group col-xs-3">
            <label for="title" class="col-xs-5">Episode</label>
            <div class="col-xs-7">
              <input type="number" step="1" min="0" class="form-control form-control-sm" name="episode" id="episode">
            </div>
          </div>
          <div class="form-group col-xs-1">
            <button type="submit" class="btn btn-primary btn-sm">Add</button>
          </div>
        </form>
      </div>`
  }

Now we need to create a model that will represent the show and "bind it" to the UI. Once again we can use the generator to create the new model.

  choo generate model show

The model will expose the properties and contains the reducer functions that will tie everything together.

Change the state of the model to look like this.

  state: {
    title: '',
    season: 0,
    episode: 0
  },

And we will add the reducer to update the property values.

  reducers: {
    updateTitle: (data, state) => ({ title: data }),
    updateSeason: (data, state) => ({ season: data}),
    updateEpisode: (data, state) => ({ episode: data})
  },

We need to tell choo about this model, so open the client.js file and add the following line.

  app.model(require('./models/show'))

Now we will modify the add-show element as follow. We create a wrapper to send events and we add an event handler in each input. We also set the value of those input elements to the property we want

  function onInput(action, send) {
    return (event) => {
      send(action, event.target.value);
      event.preventDefault();
    }
  }

  ...

  <input type="text" oninput=${onInput('show:updateTitle', send)} class="form-control form-control-sm" name="title" id="title" value="${state.show.title}">

  ...

  <input type="number" oninput=${onInput('show:updateSeason', send)} step="1" min="0" class="form-control form-control-sm" name="season" id="season" value="${state.show.season}">

  ...

  <input type="number" oninput=${onInput('show:updateEpisode', send)} step="1" min="0" class="form-control form-control-sm" name="episode" id="episode" value="${state.show.episode}">

We could add a span in the element as well to display the values and validate that the binding is correct, but that's not really what we want to do.

Let's add some tests.

Setting tape

I usually use Mocha for testing my projects, but I recently read this article about tape and though it could be a great idea to try it for this projet. Let's install it first.

  npm i tape tape-watch tap-diff --save-dev

We are adding tape-watch to be able to have the tests running watching for file changes. and tap-diff for an even nicer error message.

In the package.json file we will change the default test: script line and add a second one.

  "test": "tape tests/**/*.js",
  "test-w": "tape-watch tests/**/*.js"

We create our folder structure /tests/models and we add a file for the show model tests. We will tests the reducers, that are returning the expected values. In this case we add some tests to make sure we convert the episode and season values to integers and default to zero if the text field is emptied.

  "use strict";

  const test = require("tape"),
    show = require("../../models/show"),
    choo = require('choo'),
    app = choo();

  app.model(show);

  test("should update the show.title", (assert) => {
    const result = show.reducers.updateTitle("New title");
    assert.equal(result.title, "New title");
    assert.end();
  });

  test("should update the show.season to zero", (assert) => {
    const result = show.reducers.updateSeason("");
    assert.equal(result.season, 0);
    assert.end();
  });

  test("should update the show.season as a number", (assert) => {
    const result = show.reducers.updateSeason("12");
    assert.equal(result.season, 12);
    assert.end();
  });

  test("should update the show.episode to zero", (assert) => {
    const result = show.reducers.updateEpisode("");
    assert.equal(result.episode, 0);
    assert.end();
  });

  test("should update the show.episode as a number", (assert) => {
    const result = show.reducers.updateEpisode("30");
    assert.equal(result.episode, 30);
    assert.end();
  });

After the tests the reducers now look like this:

  updateTitle: (data, state) => {
    return {title: data};
  },
  updateSeason: (data, state) => {
    return {season: parseInt(data || 0, 10)};
  },
  updateEpisode: (data, state) => {
    return {episode: parseInt(data || 0, 10)};
  }

Saving the show

We will create a new model shows that will contain the list of shows. In some demos, the list and the properties for the individual items are all stored in the same model.

  choo generate model shows

This time since we will start by writting our tests and we will let our tests drive the design.

  "use strict";

  const test = require("tape"),
    shows = require("../../models/shows");

  test("Adding a show to the shows.llist", (assert) => {
    const data = {title: "new show", season: 1, episode: 3},
      results = shows.reducers.add(data, shows.state);

    assert.equal(results.list.length, 1, "should increase the size of the list");
    assert.equal(results.list[0].title, data.title, "should add the new show");
    assert.end();
  });

The implementation is fairly simple.

const shows = {
  namespace: 'shows',
  state: {
    list: []
  },
  reducers: {
    add: (data, state) => {
      return {list: state.list.concat(data)};
    }
  },
  effects: {
  },
  subscriptions: [
  ]
}

module.exports = shows

And we hook everything together on an event handler in the ADD button. We also add a reset action in the show model to make sure we clear up the data for the show after we add it to the list.

  function onSubmit(send, state) {
    return (event) => {
      event.preventDefault();
      send('shows:add', state.show);
      send('show:reset');
      return false;
    };
  }

  ...

  <button type="submit" onclick=${onSubmit(send, state)} class="btn btn-primary btn-sm">Add</button>

Displaying the list of shows

We will create two elements a show-list and a show. The show-list will be de table wrapper and the show element will be used to display each row.

  choo generate element show-list
  choo generate element show

The code for the show element is very simple.

  function show (show, state) {
    return html`<tr>
      <th scope="row">${show.title}</th>
      <td>${show.season}</td>
      <td>${show.episode}</td>
    </tr>`;
  }

The one for the show-list is a bit more interesting, notice how we are just mapping on the elements of the list and call the show element that is just a function.

  function shows (state) {
    return html`<table class="table">
      <thead>
        <tr>
          <th>Title</th>
          <th>Season</th>
          <th>Episode</th>
        </tr>
      </thead>
      <tbody>
      ${state.shows.list.map((s)=> {
        return show(s, state);
      })}
      </tbody>
    </table>`;
  }

We add the call to the new component in the home page and we are ready to take it for a spin.

  const html = require('choo/html'),
    navBar = require('../elements/navbar'),
    addShow = require('../elements/add-show'),
    showList = require('../elements/show-list')

  module.exports = (state, prev, send) => html`
    <main>
      ${navBar()}
      <div class="container">
        ${addShow(state, send)}
        ${showList(state)}
      </div>
    </main>`

Choo application adding data to a list

Conclusion and things to explore in future articles

Choo looks like a capable library with the basic building blocks for data driven applications. I will like to explore in future articles more real life scenarios with complex data models and reacher requirements.

Some possible articles

  • Write tests for views.
  • Replace standard with eslint for linting.
  • Edit and remove shows.
  • Add source for the TV shows (Netflix, Hulu, Crave, CBC, etc)
  • Use AJAX and effects.
  • Explore how to clean up the code a bit more and reduce the duplication.
  • Take a look at server side rendering.

Resources