riot.js, minimal MVP – Weekly Tech Learning

This post is part of my weekly tech learning series, where I take one hour each week to try out a piece of technology that I’d like to learn.

Last week I started my tech learning series back up. I’ve been feeling a bit behind with client-side JavaScript libraries so I’m planning a few around them.

To start off, I decided to try out riot.js. Billed as a minimal MVP library that is under 1kb, it felt like a good introduction before getting into the larger Angular or Ember.

Also with my pleasant experiences with knockout.js in the past, I wanted to see if riot.js would be easy add in for an existing server-side application.

Documentation

The approach riot.js takes is to add a small layer on top of regular JavaScript. The benefit here is that if you know JavaScript, its API is so small that you will barely notice it.

One thing that tripped me up was how the documentation and demo applications were built modularity. Once I understood how they plugged together it made sense, but most of my experience has been in monolithic applications.

(I’ve tried to steer client projects to a more modular structure but I’ve never been able to build one from scratch. It’s one thing to refactor towards modularity, another to start with it in mind.)

Todo list application with riot.js

Like my previous articles, the base application I’m building is the vulnerable todo list. It’s a project I’ve done many times and know the scope by heart, so I can explore how the technology side works.

This time I spent a bunch of time to really finalize my Grunt configuration. I’ve been experimenting with it over the past few months and I decided to codify those experiments into a configuration the suited me. While it was great, it did burn up a lot of my time.

Features

Being a todo list, I kept the feature-set short.

  1. Single page app without refreshes
  2. List all open todo items
  3. Add a new todo item
  4. Complete a todo item
  5. Delete a todo item

Nothing crazy.

I also didn’t want to deal with the data storage at this time. For something client-side using localStorage is probably enough but I’ve already built something using it so I didn’t need to explore how it works.

(Also riot.js is supposed to make this quite easy due to its modularity. Design your models with a simple backend API and you can swap them in or out as needed.)

The Single Page App

Since this was a single page app, I needed a base skeleton to hold the app. There’s a list (ol), a form for adding new todos, and a embedded JavaScript template for each todo item.

The final piece was to initialize and start riot.js. Initialize was easily done by calling my application’s function (todo()) with its initial configuration. As you’ll see later, this created a singleton instance of my TodoList object (a model).

One area that threw me for a few nested loops was the SPA module I copied from a demo program. This module did several things:

  • it created a singleton instance for the app
  • it would return than instance when the function was called (todo())
  • when passed a function, it would layer that function on the instance. Thereby making this new function act as a presenter layer.
  • it would initialize my TodoList model to hold the TodoItems
  • finally it would trigger the “ready” event, which presenters could listen for

All this is about 16 lines of code. Three different ways to call it (todo(), todo({..}), todo(function())). As you can imagine, this took a bit of time to understand and figure out.

Mixed with the fact that I forgot to trigger the ‘ready’ action myself, meant that I was doing some deep debugging before I understand what was happening. Once I added that, it worked exactly how I was described. Basically when the DOM is ready (from jQuery), my application is told that, and it runs any functions that were waiting on the DOM (e.g. rendering the list).

Models

Once I got everything hooked up, development started going really well. The model development in particular was easy. Models in riot.js are just basic objects that have the ability to trigger and listen for events (Observers, using riot.observable).

In this case my models didn’t use any events, they were just data stores operating on a single data structure, the todo items. Adding meant adding the TodoItem to the end of the list, completing wrapped the name in <strike>, and deleting removed the item from the list.

The real magic happened in the presenter.

Presenter

As explained above, the presenter function is layer on top of my application’s API which was just a model in this simple case.

The presenter does five different things (originally, it was actually five different presenters layered on top but it made sense to combine them into one. This is where the modularly can be handy, if you need to split up or combine presenters you can and they’d end up working the same)

  1. Listen for the form submission for adding a new todo item
  2. Listen for any clicks to complete a todo item
  3. Listen for any clicks to delete a todo item
  4. Listen for any events to redraw the list
  5. Initialize the first loaded view

If you look at the code below for the presenter’s Add, Complete, and Delete areas they are basic JavaScript/jQuery event logic. Bind on an event, run a function. Complete and Delete are ultra simple, just call the app’s function for each and then trigger a ‘list’ event to redraw the list. (app.trigger('name') is how riot.js creates new events that objects are listening for).

Add is a bit more complex because it’s working with the form. Instead of just calling the add() function, it has to pull the value out of the field, check that it’s not empty, and then clear the field when done. Nothing really out-of-the-ordinary with jQuery but it’s worth pointing out that all of this UI logic is contained in the presenter (unlike many other applications I’ve been brought in to help with).

Now the fun parts. Instead of using jQuery bindings, list is using riot’s on() function to setup a callback function when the “list” event is triggered. When this happens, the list is cleared and each item in the todo items from the model is added to the todo list. I used riot’s template rendering using the render() function and passing in each todo item. This meant my todo template (in the HTML) could access every property and riot would substitute the values before returning the final string.

The final part of the presenter is listening for the “ready” event. When this happens, the presenter knows the DOM is done and renders the first version of the page. In this case, the only dynamic thing needed is the list. Instead of duplicating how the list is rendered, the ‘list’ event is triggered which uses the list code.

Entire flow

It can be hard to see how this all goes together so here is the basic application flow from start to finish.

  1. Page loads.
  2. When the JavaScript is parsed, the presenter is added. This happens because the presenter is using todo(function()) which executes immediately. This hits the 2nd section of the todo function (labeled as SPA) which listens for the “ready” event.
  3. todo({..}) is called with a few initial, hard-coded todo items (which would be where I’d get them from the backend if I used one)
  4. That hit the 3rd section of the todo function in SPA
  5. A new TodoList was created with the todo items, which due to TodoList’s extend populated the items with the initial data
  6. Then back in todo() the instance of TodoList starts listening for the “ready” event.
  7. Sometime later the DOM finishes loading so the jQuery ready block in the HTML runs and triggers the ‘ready’ event
  8. The instance gets this ready event (3rd part of SPA) and triggers its own ready event passing in the instance as a parameter (the TodoList aka the application).
  9. This executes the presenter which sets up its own event observers (riot and jQuery) as described above.
  10. Finally the application is fully booted.

Adding a new todo item would flow like this:

  1. You enter the item into the field and click submit
  2. The presenter receives the submit event in Add, blocks it, and calls TodoList.add()
  3. add() create a random-ish id and appends the new TodoItem to the list of items
  4. Flow falls back to the presenter which resets the field, clearing it
  5. A ‘list’ event is triggered
  6. The presenter’s List observer receives the event, empties the list, and re-renders it based on the current list of items.

(As you might have noticed, I’ve had to refer to each section of the presenter by the section name given in the comments. This makes a good case for having multiple presenters which you can name separately. Even if it causes a bit more code, communication would be much easier which is a net win for any team)

Summary

All said and done, experimenting with riot.js was fun and productive. This entire process took a few of hours, some of which included reading documentation and demo code (and configuring grunt). There was more code here than in my knockout.js version but I didn’t have to embed logic into the template like I did in knockout.js (my main complaint with knockout). Instead, riot.js used custom events to handle behavior which has always been something I found useful for structuring JavaScript applications (and something I’ve done manually).

One thing I didn’t get to do was to really tease apart the model and application layers more. Right now my model and application are pretty much the same thing, which happens with such a simple concept. I’d imagine a larger application with more models would have a clearer separation between each layer.

All in all, I’m going to add riot.js to my regular toolbox. I feel it’s useful when you need a lightweight MVP library but don’t want a lot of overhead with it. Also since it’s mostly vanilla JavaScript, it should be easy to adapt to a heavier library later on should the need arise.

Code

Basic HTML page with the skeleton structure and the todo item template.

<!-- www/index.html -->
<!DOCTYPE html>
<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
 
        <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
 
        <link rel="stylesheet" href="css/normalize.css">
        <link rel="stylesheet" href="css/main.css">
        <link rel="stylesheet" href="css/app.css">
    </head>
    <body>
      <h1>Todos</h1>
 
      <ol id="todos">
      </ol>
 
      <form id="addNewTodo">
        <p>
          <input id="todoName" name="name" />
          <button type="submit">Add</button>
        </p>
      </form>
 
 
        <!-- JS -->
        <script type="text/tmpl" id="todo-item-tmpl">
          <li data-key="{item.id}">
            <a class="todo-complete" href="#">[ ]</a>
            <span>{item.name}</span> 
            <a class="todo-delete" href="#">(X)</a>
          </li>
        </script>
 
        <script type='text/javascript' src='js/jquery-2.1.0.min.js'></script>
        <script type='text/javascript' src='js/riot.min.js'></script>
        <script type='text/javascript' src='js/tech-learning-riot-js.js'></script>
        <script>
          // Initialize
          todo({items: [
            {name: "This is a test", id: 1},
            {name: "Need to do this too", id: 2},
            {name: "<strike>Done</strike>", id: 3}
          ] });
          // DOM ready so start the app
          $(function() { todo().trigger('ready'); });
        </script>
    </body>
</html>

The riot.js application. Normally you’d put each model in its own file in a directory and separate the presenters from the application API. But since this was a small app, they are combined into one file with comments to separate them.

// www/js/tech-learning-riot-js.js
function TodoList(configuration) {
  var self = riot.observable(this);
 
  // Requires an array of TodoItems
  $.extend(self, configuration);
 
  self.add = function(name) {
    var id = Math.random() * 1000000;
    var data = {name: name, id: id};
    var todo = new TodoItem(self, data);
    self.items.push(todo);
  }
 
  self.complete = function(id) {
    var itemAsArray = $.grep(self.items, function(item) { return item.id == id});
    if (itemAsArray.length > 0) {
      var item = itemAsArray[0]; // Assume only one
 
      if (item.name.match(/<strike>/)) { return false; }
      var index = $.inArray(item, self.items);
      item.name = "<strike>" + item.name + "</strike>"
      self.items[index] = item;
    }
  }
 
  self.delete = function(id) {
    self.items = $.grep(self.items, function(item) { return item.id != id});
  }
}
 
function TodoItem(app, data) {
  var self = riot.observable(this);
 
  $.extend(self, data);
}
 
// SPA
var instance;
 
window.todo = riot.observable(function(arg) {
  // todo()
  if (!arg) return instance;
 
  // todo(fn) to add a new module
  if ($.isFunction(arg)) {
      console.log('spa2'); 
    window.todo.on("ready", arg);
  } else {
    // todo(conf) to initialize the application
    instance = new TodoList(arg);
      console.log('spa3'); 
 
    instance.on("ready", function() {
      console.log('spa3-ready'); 
      window.todo.trigger("ready", instance);
    });
  }
});
 
// Presenters
todo(function(app) {
      console.log('pres-parse'); 
 
  var root = $("#todos");
  var template = $("#todo-item-tmpl").html();
 
  // Ready
  app.on("ready", function(view) {
      console.log('pres-ready'); 
    app.trigger('list');
  });
 
  /// Add
  $("#addNewTodo").submit(function(e) {
 
    e.preventDefault();
    var name = $("#todoName").val();
    if (name) {
      app.add(name);
    }
 
    this.reset();
 
    app.trigger('list');
  });
 
  /// List
  app.on("list", function(view) {
    root.empty();
 
    $.each(app.items, function(i, item) {
      root.append(
        riot.render(template, { item: item }));
    });
  });
 
  /// Complete
  $('body').on('click', '.todo-complete', function(e) {
    e.preventDefault();
 
    app.complete($(this).parent().data('key'));
 
    app.trigger('list');
  });
 
  /// Delete
  $('body').on('click', '.todo-delete', function(e) {
    e.preventDefault();
 
    app.delete($(this).parent().data('key'));
 
    app.trigger('list');
  });
});