Simple ToDo list web application with node.js, Express, and Riot

0
4482
10 min read

The frontend space is indeed crowded, but none of the more popular solutions are really convincing to me. I feel Angular is bloated and the double binding is not for me. I also do not like React and its syntax. Riot is, as stated by their creators, “A React-like user interface micro-library” with simpler syntax that is five times smaller than React.

What we are going to learn

We are going to build a simple Riot application backed by Express, using Jade as our template language. The backend will expose a simple REST API, which we will consume from the UI. We are not going to use any other dependency like JQuery, so this is also a good chance to try XMLHttpRequest2. I deliberately ommited the inclusion of a client package manager like webpack or jspm because I want to focus on the Expressjs + Riotjs. For the same reason, the application data is persisted in memory.

Requirements

You just need to have any recent version of node.js(+4), text editor of your choice, and some JS, Express and website development knowledge.

Project layout

Under my project directory we are going to have 3 directories:


* public For assets like the riot.js library itself.

* views Common in most Express setup, this is where we put the markup.

* client This directory will host the Riot tags (we will see more of that later) We will also have the package.json, our project manifesto, and an app.js file, containing the Express application. Our Express server exposes a REST API; its code can be found in api.js.

Here is how the layout of the final project looks:

├── api.js
├── app.js
├── client
│   ├── tag-todo.jade
│   └── tag-todo.js
├── package.json
├── node_modules
├── public
│   └── js
│       ├── client.js
│       └── riot.js
└── views
    └── index.jade

Project setup

Create your project directory and from there run the following to install the node.js dependencies:

$ npm init -y
$ npm install --save body-parser express jade

And the application directories:

$ mkdir -p views public/js client

Start!

Lets start by creating the Express application file, app.js:

'use strict';
const express = require('express'),
    app = express(),
    bodyParser = require('body-parser');

// Set the views directory and template engine
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');

// Set our static directory for public assets like client scripts
app.use(express.static('public'));

// Parses the body on incoming requests
app.use(bodyParser.json());

// Pretty prints HTML output
app.locals.pretty = true;

// Define our main route, HTTP "GET /" which will print "hello"
app.get('/', function (req, res) {
   res.send('hello');
});


// Start listening for connections
app.listen(3000, function (err) {
    if (err) {
        console.error('Cannot listen at port 3000', err);
    }
    console.log('Todo app listening at port 3000');
});

The #app object we just created, is just a plain Express application. After setting up the application we call the listen function, which will create an HTTP server listening at port 3000.

To test our application setup, we open another terminal, cd to our project directory and run $ node app.js.

Open a web browser and load http://localhost:3000; can you read “hello”?
Node.js will not reload the site if you change the files, so I recommend you to install nodemon.

Nodemon monitors your code and reloads the site on every change you perform on the JS source code. The command,$ npm install -g nodemon, installs the program on your computer globally, so you can run it from any directory.

Okay, kill our previously created server and start a new one with $ nodemon app.js.

Our first Riot tag

Riot allows you to encapsulate your UI logic in “custom tags”. Tag syntax is pretty straightforward. Judge for yourself.

<employee>
<span>{ name }</span>
</employee>

Custom tags can contain code and can be nested as showed in the next code snippet:

<employeeList>
<employee each="{ items }" onclick={ gotoEmployee } />
<script>
  gotoEmployee (e) {
    var item = e.item;
    // do something
  } 
</script>
</employeeList>

This mechanism enables you to build complex functionality from simple units. Of course you can find more information at their documentation.

On the next steps we will create our first tag: ./client/tag-todo.jade.

Oh, we have not yet downloaded Riot! Here is the non minified Riot + compiler download. Download it to ./public/js/riot.js.

Next step is to create our index view and tell our app to serve it. Locate / router handler, remove the res.send(‘hello) ‘, and update to:

// Define our main route, HTTP "GET /" which will print "hello"
app.get('/', function (req, res) {
   res.render('index');
});

Now, create the ./views/index.jade file:

doctype html
html
    head
        script(src="/js/riot.js")
    body
        h1 ToDo App
        todo

Go to your browser and reload the page. You can read the big “ToDo App” but nothing else. There is a <todo></todo> tag there, but since the browser does not understand, this tag is not rendered. Let’s tell Riot to mount the tag. Mount means Riot will use <todo></todo> as a placeholder for our —not yet there— todo tag.

doctype html
html
    head
        script(src="/js/riot.js")
    body
        h1 ToDo App
        script(type="riot/tag" src="/tags/todo.tag")
        todo
        script.
            riot.mount('todo');

Open your browser’s dev console and reload the page. riot.mount failed because there was no todo.tag.

Tags can be served in many ways, but I choose to serve them as regular Express templates. Of course, you can serve it as static assets or bundled. Just below the / route handler, add the /tags/:name.tag handler.

// "/" route handler
app.get('/', function (req, res) {
    res.render('index');
});

// tag route handler
app.get('/tags/:name.tag', function (req, res) {
    var name = 'tag-' + req.params.name;
    res.render('../client/' + name);
});

Now create the tag in ./client/tag-todo.jade:

todo
    form(onsubmit="{ add }")
        input(type="text", placeholder="Needs to be done", name="todo")

And reload the browser again. Errors gone and a new form in your browser.
onsubmit=”{ add }” is part of Riot’s syntax and means on submit call the add function. You can add mix implementation with the markup, but I rather prefer to split markup from code. In Jade (and any other template language),it is trivial to include other files, which is exactly what we are going to do. Update the file as:

todo
    form(onsubmit="{ add }")
        input(type="text", placeholder="Needs to be done", name="todo")
    script
        include tag-todo.js

And create ./client/tag-todo.js with this snippet:

'use strict';
var self = this;
var api = self.opts;

When the tag gets mounted by Riot, it gets a context. That is the reason for var self = this;. That context can include the opts object. opts object can be anything of your choice, defined at the time you mount the tag. Let’s say we have an API object and we pass it to riot.mount as the second option at the time we mount the tag, that isriot.mount(‘todo’, api).

Then, at the time the tag is rendered this.opts will point to the api object. This is the mechanism we are going to use to expose our client api with the todo tag.

Our form is still waiting for the add function, so edit the tag-todo.js again and append the following:

self.add = function (e) {
    var title = self.todo.value;
    console.log('New ToDo', title);
};

Reload the page, type something at the text field, and hit enter. The expected message should appear in your browser’s dev console.

Implementing our REST API

We are ready to implement our REST API on the Express side. Create ./api.js file and add:

'use strict';
const express = require('express');
var app = module.exports = express();

// simple in memory DB
var db = [];

// handle ToDo creation
app.post('/', function (req, res) {
    db.push({
        title: req.body.title,
        done: false
    });
    let todoID = db.length - 1;
    // mountpath = /api/todos/
    res.location(app.mountpath + todoID);
    res.status(201).end();
});

// handle ToDo updates
app.put('/', function (req, res) {
    db[req.body.id] = req.body;
    res.location('/' + req.body.id);
    res.status(204).end();
});

Our API supports ToDo creation/update, and it is architected as an Express sub application. To mount it, we just need to update app.js for the last time. Update the require block at app.js to:

const express = require('express'),
    api = require('./api'),
    app = express(),
    bodyParser = require('body-parser');
...

And mount the api sub application just before the app.listen…

// Mount the api sub application
app.use('/api/todos/', api);

We said we will implement a client for our API. It should expose two functions –create and update –located at ./public/client.js. Here is its source:

'use strict';
(function (api) {
    var url = '/api/todos/';

    function extractIDFromResponse(xhr) {
        var location = xhr.getResponseHeader('location');
        var result = +location.slice(url.length);
        return result;
    }
    api.create = function createToDo(title, callback) {
        var xhr = new XMLHttpRequest();
        var todo = {
            title: title,
            done: false
        };
        xhr.open('POST', url);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function () {
            if (xhr.status === 201) {
                todo.id = extractIDFromResponse(xhr);
            }
            return callback(null, xhr, todo);
        };
        xhr.send(JSON.stringify(todo));
    };
    api.update = function createToDo(todo, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('PUT', url);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function () {
            if (xhr.status === 200) {
                console.log('200');
            }
            return callback(null, xhr, todo);
        };
        xhr.send(JSON.stringify(todo));
    };
})(this.todoAPI = {});

Okay, time to load the API client into the UI and share it with our tag. Modify the index view including it as a dependency:

doctype html
html
    head
        script(src="/js/riot.js")
    body
        h1 ToDo App
        script(type="riot/tag" src="/tags/todo.tag")
        script(src="/js/client.js")
        todo
        script.
            riot.mount('todo', todoAPI);

We are now loading the API client and passing it as a reference to the todo tag. Our last change today is to update the add function to consume the API.

Reload the browser again, type something into the textbox, and hit enter.
Nothing new happens because our add function is not yet using the API. We need to update ./client/tag-todo.js as:

'use strict';
var self = this;
var api = self.opts;
self.items = [];

self.add = function (e) {
    var title = self.todo.value;
    api.create(title, function (err, xhr, todo) {
        if (xhr.status === 201) {
            self.todo.value = '';
            self.items.push(todo);
            self.update();
        }
    });
};

We have augmented self with an array of items. Everytime we create a new ToDo task (after we get the 201 code from the server) we push that new ToDo object into the array because we are going to print that list of items.

In Riot, we can loop the items adding each attribute to any tag. Last, update to ./client/tag-todo.jade

todo
    form(onsubmit="{ add }")
        input(type="text", placeholder="Needs to be done", name="todo")
    ul
        li(each="{items}")
            span {title}

    script
        include tag-todo.js

Finally! Reload the page and create a ToDo!

Next steps

You can find the complete source code for this article here. The final version of the code also implements a done/undone button, which you can try to implement by yourself.

About the author

Pedro NarcisoGarcíaRevington is a Senior Full Stack Developer with 10+ years experience in high scalability and availability, microservices, automated deployments, data processing, CI, (T,B,D)DD and polyglot persistence.

LEAVE A REPLY

Please enter your comment!
Please enter your name here