Learn AbsurdJS: Building a to-do list app

You've probably heard about the ToDoMVC project. It's takes a basic to-do list application and replicates it with different frameworks. It's interesting to see how the same problem is solved by different programmers following different concepts. In this article, we'll make the standard ToDoMVC application with AbsurdJS, a framework that can convert JavaScript (or JSON) to valid CSS and HTML.

AbsurdJS introduction

AbsurdJS started as a CSS preprocessor, which I distributed as Node.js module. In the process of development it transformed into an HTML preprocessor, and very soon after that it was translated for client side usage. At the moment, it's roughly 80 KB and can convert JavaScript to CSS and HTML.

The client-side version of AbsurdJS -- the one that we're going to use -- covers most of a modern frameworks' capabilities.

  • Component driven
  • Data binding
  • DOM event handling
  • Dependency injection
  • Template engine
  • Built-in router, AJAX wrapper and DOM helper

However, there is something fundamentally different. The framework can convert JavaScript (or JSON) to valid CSS and HTML. Moreover, when we talk about client-side development, we know that we have to write a lot of CSS and HTML. AbsurdJS gives us the power to write in one language - JavaScript. As we will see in the next sections, we will write everything into JavaScript files. Including the CSS styling.

Project setup

The idea of the ToDoMVC project is that every developer starts with same assets. There's a template and CSS that we have to use. Our project looks like that:

/absurd
/bower_components
    /todomvc-common
/css
    /app.css
/js
    /model
        /model.js
    /views
        /footer.js
        /header.js
        /main.js
    /app.js
/index.html

The absurd directory contains the absurd.min.js (the library itself) and absurd.organic.min.js, which is a collection of CSS mixins. bower_components folder delivers the default look of the application, the basic CSS styling and images that we mentioned above. The app.css file is where we have to add our custom stylesheets. However, we're going to use JavaScript for the styling, so in the end the file will be almost empty. The js directory contains our logic.

Development with AbsurdJS is close to Backbone development. Thew View plays the role of the controller. We could make another comparison and say that the framework is similar to Facebook's React. Everything in AbsurdJS is a component.

Here's the skeleton of our ToDoMVC app:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Template • TodoMVC</title>
        <link rel="stylesheet" href="bower_components/todomvc-common/base.css">
        <link rel="stylesheet" href="css/app.css">
    </head>
    <body>
        <script src="absurd/absurd.organic.min.js"></script>
        <script src="absurd/absurd.min.js"></script>
        <script src="js/app.js"></script>
        <script src="js/model/model.js"></script>
        <script src="js/views/header.js"></script>
        <script src="js/views/main.js"></script>
        <script src="js/views/footer.js"></script>
        <script type="text/javascript">if(!NREUMQ.f){NREUMQ.f=function(){NREUMQ.push(["load",new Date().getTime()]);var e=document.createElement("script");e.type="text/javascript";e.src=(("http:"===document.location.protocol)?"http:":"https:")+"//"+"js-agent.newrelic.com/nr-100.js";document.body.appendChild(e);if(NREUMQ.a)NREUMQ.a();};NREUMQ.a=window.onload;window.onload=NREUMQ.f;};NREUMQ.push(["nrfj","beacon-1.newrelic.com","7d8608a34f","3053298","YFdVYEsAVxdYAhAICVkddldNCFYKFhQXBBQYRkJAVhNQBVUSSwQCXkY=",0,91,new Date().getTime(),"","","","",""]);</script>
    </body>
</html>

We included the CSS styles at the top of the page and the scripts at the bottom.

The header

Our HTML starts with a simple header

<header id="header">
    <h1>todos</h1>
    <input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>

ToDoMVC with AbsurdJS

The main area

Just after that we have an area that contains the list with the ToDo items.

<section id="main">
    <input id="toggle-all" type="checkbox">
    <label for="toggle-all">Mark all as complete</label>
    <ul id="todo-list">
        <li>
            <div class="view">
                <input class="toggle" type="checkbox">
                <label>Task 1</label>
                <button class="destroy"></button>
            </div>
            <input class="edit" value="">
        </li>
    </ul>
</section>

ToDoMVC with AbsurdJS

Footer

In the end, we have a footer. It contains some informational spots and filtering navigation.

<footer id="footer">
    <span id="todo-count">
        <strong>0</strong> item 0 left
    </span>
    <ul id="filters">
        <li><a class="" href="#/">All</a></li>
        <li><a class="" href="#/active">Active</a></li>
        <li><a class="" href="#/completed">Completed</a></li>
    </ul>
    <button id="clear-completed">Clear completed (0)</button>
</footer>

ToDoMVC with AbsurdJS

The CSS styles

There is only one thing that we'll do in css/app.css. We'll hide the footer and the main section. It's not because we can't do that in JavaScript. It's because the JavaScript needs time to boot, and the user may see something before that. So:

#footer, #main {
    display: none;
}

Defining a namespace

It's good practice to work in a private namespace. If we expose everything to the global scope, we may have collisions with other frameworks or libraries. Here's how our js/app.js starts:

(function( window ) {
    'use strict';

    window.absurd = Absurd(); // AbsurdJS API
    window.App = {}; // namespace

})( window );

use strict puts your code in strict mode. John Resig posted a nice article revealing more information about that. In general:

  • It catches some common coding bloopers, throwing exceptions.
  • It prevents, or throws errors, when relatively "unsafe" actions are taken (such as gaining access to the global object).
  • It disables features that are confusing or poorly thought out.

It's a good way to write a little bit better code.

When we include AbsurdJS in the page, we have access to a global Absurd function that returns the library's API. We store that in window.absurd. All the other code that we write should be under window.App namespace.

AbsurdJS components and dependency injection

Before we proceed with the actual implementation, we should say a few words about AbsurdJS components and the integrated dependency injection. The typical component looks like this:

var ComponentClass = absurd.component('Name', {
    constructor: function() {
        // ...
    },
    doSomething: function() {
        this.dispatch('some-event', { data: 42 });
    }
});

var component = ComponentClass();
component.on('some-event', function(event) {
    console.log('The answer is ' + event.data);
}).doSomething();

The component API returns a function. We could call this function and create as many instances of the component as we need. The syntax is similar to the one used in Backbone.js -- we also send our logic as an object. The entry point of the component is the constructor method. By default, every instance is an event dispatcher. In the example above, we're subscribing to some-event. Just after that we fire the doSomething function, which internally dispatches the event.

Sooner or later, we start thinking about managing dependencies. Different parts of our application need different modules, and it's ideal if we can elegantly deliver them. To achieve that, AbsurdJS implements AngularJS's dependency injection. Here's how it works:

absurd.di.register('mymodule', {
    doSomething: function() {
        console.log('Hi!');
    }
});

absurd.component('Name', {
    doSomething: function(mymodule) {
        mymodule.doSomething();
    }
})().doSomething();

We first register our dependency via the absurd.di.register function. It could be anything: a function, object or maybe a string. Afterwards, we just type our module as a parameter. The framework automatically calls the function with the right arguments. It's very important to keep the same name used in the register method.

Writing the model

In the typical MVC pattern, the model is a class that stores and manages our data. We'll stick to this idea and write our model in that manner.

We'll store the ToDos of our application in the local storage of the browser. So, it makes sense to have a wrapper around this functionality. So, we'll define it as a dependency so we have easy access to it later.

// js/app.js
absurd.di.register('storage', {
    key: 'todos-absurdjs',
    put: function(todos) {
        window.localStorage.setItem(this.key, JSON.stringify(todos));
    },
    get: function() {
        if(window.localStorage) {
            var value = window.localStorage.getItem(this.key);
            return value != null ? JSON.parse(value) : [];
        }
        return [];
    }
});

We have just two methods - put and get. They deal with the window.localStorage. The first one accepts the array containing the ToDos and the second one returns it.

Now, let's use it as dependency and start filling our model with functions:

// js/model/model.js
App.Model = absurd.component('Model', {
    data: [],
    constructor: function(storage) {
        this.data = storage.get();
    },
    updated: function(storage) {
        storage.put(this.data);
    },
    add: function(text) {
        this.data.push({
            title: text,
            completed: false
        });
        this.dispatch('updated');
    }
});

Once our model is initialized we try to get the data from storage. Notice that we are injecting the storage object. data is a property of our class that holds the ToDos. There is also an add method. We send the text of the ToDo, and the component makes a new entry. Moreover, the same method dispatches an updated event. In AbsurdJS, the component could catch its own events. All we have to do is define a function with the same name.

We will modify the data array in various situations. There are parts of our user interface that are interested in these changes. These parts should be notified, and the Dispatching updated event guarantees that. At the same time, the model itself needs to update the content of the local storage. So, we could simply add an updated method. It will be called once the event with the same name is dispatched.

By definition, the ToDoMVC app needs to do a few other operations. Like, for example, toggling, removing or editing. It should also show some information about how many ToDos are completed or left. Here's the list of methods that cover these functionalities:

toggle: function(index, completed) {
    this.data[index].completed = completed;
    this.dispatch('updated');
},
changeTitle: function(title, index) {
    if(title === '') {
        this.remove(index);
    } else {
        this.data[index].title = title;    
    }
    this.dispatch('updated');
},
toggleAll: function(completed) {
    for(var i=0; i<this.data.length; i++) {
        this.data[i].completed = completed;
    }
    this.dispatch('updated');
},
remove: function(index) {
    this.data[index] ? this.data.splice(index, 1) : null;
    this.dispatch('updated');
},
all: function() {
    return this.data.length;
},
left: function() {
    return this.todos('active').length;
},
completed: function() {
    return this.todos('completed').length;
},
areAllCompleted: function() {
    return this.todos('completed').length == this.todos().length;
},
todo: function(index) {
    return this.data[index];
},
todos: function(filter) {
    var arr = [];
    switch(filter) {
        case 'active': 
            for(var i=0; i<this.data.length; i++) {
                if(!this.data[i].completed) arr.push(this.data[i])
            }
        break;
        case 'completed': 
            for(var i=0; i<this.data.length; i++) {
                if(this.data[i].completed) arr.push(this.data[i])
            }
        break;
        default: arr = this.data;
    }
    return arr;

},
clearCompleted: function() {
    this.data = this.todos('active');
    this.dispatch('updated');
}

We have methods that give us access to the items stored in the data array. In some cases, we need the ToDo presented as a JavaScript object. So, defining methods like todo save us time. Also, the todos function accepts a filter setting and makes the cut.

Bootstrapping the application

In programming, we always have an entry point. In our case it'll be in the js/app.js file. Let's create a component that acts as an arbiter. It will create instances from the model, header, main and footer classes.

// js/app.js
absurd.component('Application', {
    ready: function() {
        var model = App.Model();
    }
})();

We define a new component class called Application and immediately create an instance from it. We write code that lives in the browser so, in most of the cases we are interested in running it once the page is fully loaded. Every AbsurdJS component could have ready method. It is, of course, optional but if it's set the framework calls it when the DOM is ready.

We only have the model defined, so we initialize it. The model variable will be sent to the other parts of the application.

Adding new ToDo items

The HTML markup that is responsible for adding a new ToDo is positioned in the header.

<header id="header">
    <h1>todos</h1>
    <input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>

AbsurdJS works with dynamically created DOM elements. Moreover, it supports fetching elements from the current DOM tree. In this article, we're not going to use templates defined in JavaScript. More information about that is listed here. We will work with the markup that is already in the page.

Here is the finished version of our Header class:

// js/views/header.js
App.Header = absurd.component('Header', {
    html: '#header',
    onInputChanged: function(e) {
        if(e.keyCode == 13 && e.target.value.toString().trim() != '') {
            this.model.add(e.target.value.trim());
            e.target.value = '';
        }
    },
    constructor: function(model) {
        this.model = model;
        this.populate();
    }
});

Let's examine it piece-by-piece:

  • html: '#header' tells AbsurdJS that this component works with an element matching #header selector.
  • The constructor of the component accepts the model and calls the internal function populate. It's the only "magical" function in the framework. It does several things like fetching the right DOM element, parsing it as a template, compiling CSS and adding events' listeners. After the calling of this method, we have access to this.el property that points to the actual DOM element.
  • onInputChanged - this is an event handler that has to be attached to the input field. It checks if the user presses the Enter key. If yes it calls the add method of the model and clears the field.

The Header class looks ok. However, it does not do anything right now because there is no event attached. To make the things work, we do not have to update our JavaScript. We need to set the data-absurd-event attribute in HTML:

<header id="header">
    <h1>todos</h1>
    <input 
        id="new-todo"
        placeholder="What needs to be done?" 
        data-absurd-event="keyup:onInputChanged" 
        autofocus>
</header>

And of course, we have to create an instance from the class in app.js:

// js/app.js
var model = App.Model(),
    header = App.Header(model);

Displaying ToDos

Let's say that we have data in our storage. We need to show the ToDos on the screen. Let's start filling js/views/main.js file:

// js/views/main.js
App.Main = absurd.component('Main', {
    html: '#main',
    filter: 'all',
    todos: [],
    constructor: function(model) {
        this.model = model;
        this.todos = this.model.todos(this.filter);
        this.populate();
    }
});

Still the same pattern. We define the component and set the value to the html property. The model is passed to the constructor. We fetched the current ToDos and call the populate method. The app.js file needs one more line:

var model = App.Model(),
    header = App.Header(model),
    main = App.Main(model);

So far so good. If we open the application now, we won't see any results. It's because we did not update our template. In other words, if we want to show something we have to add expressions. By expressions, I mean code that means something to the framework. The current template is as follows:

<section id="main">
    <input id="toggle-all" type="checkbox">
    <label for="toggle-all">Mark all as complete</label>
    <ul id="todo-list">
        <li>
            <div class="view">
                <input class="toggle" type="checkbox">
                <label>Task 1</label>
                <button class="destroy"></button>
            </div>
            <input class="edit" value="">
        </li>
    </ul>
</section>

We'll change it to:

<section id="main">
    <input id="toggle-all" type="checkbox">
    <label for="toggle-all">Mark all as complete</label>
    <ul id="todo-list">
        <% for(var i=0; todo = todos[i]; i++) { %>
        <li>
            <div class="view">
                <input class="toggle" type="checkbox">
                <label>Task 1</label>
                <button class="destroy"></button>
            </div>
            <input class="edit" value="">
        </li>
        <% } %>
    </ul>
</section>

We wrapped the <li> tag in a for loop. There are two things that we have to mention here:

  • There is no new language or syntax between <% and %>. The expressions are pure JavaScript.
  • The expressions are evaluated in the context of the component. So we have access to every property or method of that component. In our case, we're using the todos property.

Now let's add some ToDos and check what's going on:

ToDoMVC with AbsurdJS

We do not have logic that updates the UI when a new ToDo is added, so we have to refresh the page. What we see is that there are two new <li> tags added, but they are not visible. And they are not visible because we set display: none to the #main container. We did:

// css/app.css
#footer, #main {
    display: none;
}

We need to change that. Here's the moment where AbsurdJS becomes really handy. Normally, when we're in such a situation we:

  • Create a new CSS class like .main-visible that has display: block in it.
  • Set the style manually to the element.

With AbsurdJS, it's a bit different. At the beginning of the article, we said that this is a library that started as CSS preprocessor (i.e. it converts JavaScript to CSS). In the client-side context, this could be used for CSS injection. Let's change our class so it shows the container if there are any ToDos:

// js/views/main.js
App.Main = absurd.component('Main', {
    html: '#main',
    filter: 'all',
    todos: [],
    css: {
        '#main': {
            display: '<% model.all() == 0 ? "none" : "block" %>'
        }
    },
    constructor: function(model) {
        this.model = model;
        this.todos = this.model.todos(this.filter);
        this.populate();
    }
});

We are able to use expressions in the CSS, too. model.all() returns the number of the ToDos in the list. All we have to do is call this.populate() and the framework will grab the content of the css property, convert it to valid CSS and inject it into the page.

ToDoMVC with AbsurdJS

We need to subscribe to the updated event of the model so we can update the interface when the model changes. It makes sense to create a separate function:

// js/views/main.js
App.Main = absurd.component('Main', {
    html: '#main',
    filter: 'all',
    todos: [],
    css: {
        '#main': {
            display: '<% model.all() == 0 ? "none" : "block" %>'
        }
    },
    constructor: function(model, router) {
        this.model = model;
        model.on('updated', this.bind(this.update));
        this.update();
    },
    update: function(filter) {
        this.filter = filter || this.filter;
        this.todos = this.model.todos(this.filter);
        this.populate();
    }
});

After this change, we are able to see the newly added entries. However, the label of the ToDo in the browser is still Task 1. So:

<label>Task 1</label>

should be changed to:

<label><% todo.title %></label>

Here is the result so far:

ToDoMVC with AbsurdJS

Removing, editing and toggling ToDos

Our code can add new ToDos. We will continue with the rest of the tasks - removing, editing and toggling.

Deleting an entry

We have a button reserved for the purpose. Its markup is as follows:

<button class="destroy"></button>

And we will change it to:

<button class="destroy" data-absurd-event="click:removeToDo:<% i %>"></button>

Similar to the previous section, we added an event listener. However, this time we are doing something more. We make our function accept an argument, and this is the current index of the ToDo. Here is how removeToDo looks like:

// js/views/main.js
removeToDo: function(e, index) {
    this.model.remove(index);
}

Notice that the event handler receives the usual event object first. Our custom parameter is sent as a second argument. The model does the rest of the task. The UI is automatically updated because we are subscribed to the model's updated` event.

Toggling the ToDos

The ToDoMVC folks require a completed class to every item that we mark as done. AbsurdJS compares its virtual DOM to the one in the actual tree and makes the necessary changes. So, all we just have to add one conditional statement that checks the completed flag of the ToDo:

<li class="<% todo.completed ? 'completed' : '' %>">

This is enough to update the list. Every time we call populate AbsurdJS updates its virtual DOM element, and it'll transfer that change to the page if the class property is updated. Here's the new method for toggling:

// js/views/main.js
toggleToDo: function(e, index) {
    this.model.toggle(index, e.target.checked);
}

We have to update the HTML, so we call toggleToDo when the user clicks on the checkbox.

<input class="toggle" type="checkbox">

Became:

<input class="toggle" type="checkbox" data-absurd-event="click:toggleToDo:<% i %>">

The result looks like this:

ToDoMVC with AbsurdJS

We could add one more function that will toggle all the entries:

toggleAll: function(e) {
    this.model.toggleAll(e.target.checked);
}

And attach it to the element with #toggle-all id:

<input id="toggle-all" type="checkbox" data-absurd-event="click:toggleAll">

Editing

The editing happens when the user double clicks an item in the list. The data-absurd-event attribute should be set to the <li> tag:

<li 
    class="<% todo.completed ? 'completed' : '' %>" 
    data-absurd-event="dblclick:edit:<% i %>"
>

We need to make one more modification to show an input field. In that field, the user will type the new value. At the moment we have:

<input class="edit" value="">

We'll change it to:

<input 
    class="edit" 
    value="" 
    data-absurd-event="keyup:onInputChanged:<% i %>, blur:save:<% i %>"
>

Notice that we're calling onInputChanged along with another function called save. AbsurdJS accepts multiple event handlers separated by commas. We may add as many as we want. For that particular element, we need to catch the Enter and Esc keys. So we save or discard the changes. When a user leaves the field we should save, too. This is the reason behind the blur event listening.

Here is the logic behind onInputChanged and save:

onInputChanged: function(e, index) {
    if(e.keyCode == 13) {
        this.save(e, index);
    } else if(e.keyCode == 27) {
        e.target.value = this.currentTitle;
        this.save(e, index);
    }
},
save: function(e, index) {
    this.model.changeTitle(e.target.value.trim(), index);
}

Improving UI after population

Imagine that we mark all the ToDos as done with the #toggle-all button. The button itself has a small icon that is changed into a different color. This is all nice but we should make sure that we return the initial color if some of the ToDos are unchecked. We'll use the populated function:

populated: function() {            
    var checkboxes = this.qsa('.toggle');
    for(var i=0; i<checkboxes.length; i++) {
        checkboxes[i].checked = this.todos[i].completed;
    }
    this.qs('#toggle-all').checked = this.model.areAllCompleted();
}

That function is called when populate method finishes its job. Notice that in this example we are using this.qs and this.qsa that are just shortcuts to document.querySelector and document.querySelectorAll. What we are doing above is just checking if the current entries are all selected. If not, then we update the checked property of the toggle button.

The footer

The footer shows information about the currently selected ToDos and performs filtering. It also has a button for clearing the completed records. We again need some additions to the HTML:

<footer id="footer">
    <span id="todo-count">
        <strong><% this.model.left() %></strong> 
        item<% this.model.left() == 1 ? '' : 's' %> left
    </span>
    <ul id="filters">
        <li>
            <a class="<% this.filterIndex === 0 ? 'selected' : '' %>" href="#/">All</a>
        </li>
        <li>
            <a class="<% this.filterIndex === 1 ? 'selected' : '' %>" href="#/active">Active</a>
        </li>
        <li>
            <a class="<% this.filterIndex === 2 ? 'selected' : '' %>" href="#/completed">Completed</a>
        </li>
    </ul>
    <% if(this.model.completed() > 0) { %>
    <button id="clear-completed" data-absurd-event="click:clearCompleted">Clear completed (<% this.model.completed() %>)</button>
    <% } %>
</footer>

At the top of the snippet, we show the remaining ToDos. The unordered list contains three links that show all the entries, only the active ones and only the completed ones. In the end, we conditionally show the button that removes the finished ToDos.

Here's the code that we have to place in js/views/footer.js:

App.Footer = absurd.component('Footer', {
    html: '#footer',
    filterIndex: 0,
    css: {
        '#footer': {
            display: '<% model.all() == 0 ? "none" : "block" %>'
        }
    },
    constructor: function(model) {
        this.model = model;
        this.model.on('updated', this.bind(this.update));
        this.update();
    },
    update: function(filterIndex) {
        this.filterIndex = typeof filterIndex != 'undefined' ? filterIndex : this.filterIndex;
        this.populate();
    },
    clearCompleted: function() {
        this.model.clearCompleted();
    }
});

It looks a lot like js/views/main.js in the beginning. We again have CSS injection that depends on model.all(). The model is passed to the component, and we subscribe to its updated event. The clearCompleted method simply forwards the task to the model. We should also add one more line to js/app.js so we get our footer class initialized:

footer = App.Footer(model);

Now if we refresh the page we see that everything works perfectly except the filtering. We got #/active and completed in the bar, but nothing happens. It is because we do not have any logic that handles these changes in the URL.

AbsurdJS has a built-in router that works with the good old hash type of navigation but also supports the History API. Let's change the ready function of js/app.js to the following:

ready: function(router) {
    var model = App.Model(),
        header = App.Header(model),
        main = App.Main(model),
        footer = App.Footer(model);

    router
    .add(/active\/?$/, function() {
        main.update('active');
        footer.update(1);
    })
    .add(/completed\/?$/, function() {
        main.update('completed');
        footer.update(2);
    })
    .add(function() {
        main.update('all');
        footer.update(0);
    })
    .listen(10) // listening for route changes
    .check(); 
}

Because the router is part of AbsurdJS, it is also available for dependency injection. We simply drop it as an argument. The class accepts regular expressions and compares them to the current URL. If some match, it calls a function. The method listen fires the check method every ten milliseconds.

Summary

AbsurdJS is a client-side framework that aims to provide simplicity. It sticks to the JavaScript object literals for defining classes. It has powerful template processing, dependency, and CSS injection.

If you're interested in using it, check out the official site at http://absurdjs.com/.

0 comments


Or enter your name and Email
No comments have been posted yet.