October 24, 2014

In this article we’ll try to implement a minimal data-binding framework using new fancy Object.observe API which everyone’s in love with. We’ll go step by step and try to solve all of the problems appear on our way. The resulting framework is not by any means production-ready, though I’ll publish it on Github for this tutorial needs only.

Modern MVC frameworks like AngularJS or EmberJS gained their popularity for saving developer’s time in the era of ajax web-oriented applications. Mostly this is done by implementing data-to-html binding, so that a developer can focus on code and don’t care about the views much — they are updated accordingly automatically. This removes the need of working with DOM directly and thus reduces the effort dramatically.

Until very recent time, there were only two ways of binding your data.

One is putting your model object into a wrapper with the ability to set or get properties via some special methods like set(name, value) and get(name) (see Backbone for example). This allows triggering views re-rendering easily, because it is clear when and what has changed.

Another way is so called ‘dirty-checking’. That is when you use plain JavaScript objects as models and check every such objects for changes every time any possible value-changing operation happens (like networking or DOM events). This approach is used in angular. On one hand this can be quite slow (but read what Misko says), on the other hand it brings the simplicity and charm of vanilla JavaScript objects.

And now we have a quite new approach for the problem, the Object.observe . As you may already guessed it allows you to monitor objects for changes and run callbacks natively, which eliminates the need for dirty-checking. Developers wanted that one so much, that it is already in stable Chrome, even though formally this is ES7 area. Chrome is pretty much the only one that implemented it, but others are on their way and there’s a polyfill already.

Object.observe is a native API, that allows you to monitor object for changes and run callbacks for example when the property gets deleted. It is easy to check what we can do with it. Open Chrome console and run:

var model = { }; Object.observe(model, function(changes) { debugger; }); model.name = 'value'

Now when you’re in debug mode, you can print out changes and see that this is an array, each elements is a change. The only one element in this case has several properties: type (what happened), object (just a pointer to the observed object), name (what has changed). If there is an old value (which is not the case in this example), you will also have oldValue . Pretty cool, huh?

A couple of things to note here.

Observing is shallow. Which means that you won’t be notified if some nested object has changed, which is kinda pity. Imagine that we have an array of users and every user has the nested info object with phone number. Changing phone number in that case won’t notify anyone that array has changed and that we need to re-render the whole view. That is why we will have to implement something like deepObserve .

Also, keep in mind that several very fast changes will not result in observer function being called each time. More likely it will fire only once but with several changes.

Now when you generally understand how the mechanics work we can start with something real.

Movies lookup app #

We will make a simple one-page app in which user can lookup movies. The main idea is when user type some title in the search bar on the left, the query is sent to the server and the results are displayed on the right. You can play with the working example here.

Neat, isn’t it?

Implementing the app #

For the purpose of this tutorial we will use jquery, though I know, I know, I’d be better off with native API, but I’m used to it and it’s actually doesn’t matter, you can use zeptojs or your own overhead-free implementation. We will also use underscorejs mostly because it’s super fancy and all the cool kids are using it. And we will also need some template engine. Handlebars, perhaps, but you will see that we can easily use any other. For styling we’ll use guess what? Yep!

Let’s start with html, cause this is something that describes what we aim to achieve. It looks a lot like angular, but this is just cause, well, they get it right.

<body> ... <div id="main" mb-scope="moviesApp.searchPage"> ... <div class="results" mb-bind="results"> {{#each results}} <div class="item"> <!-- ... more templating stuff ... --> </div> {{else}} <div class="noData">No results found.</div> {{/each}} </div> ... </div> </div>

Few things to notice here. mb-bind="results" is our way of saying that everything that inside of this depends on that results property and should be re-rendered every time it changes. Inside there’s a typical handlebars template, that iterates over results and displays movies or, in case if nothing found, renders a stub.

Then the page scope mb-scope="moviesApp.searchPage" , which basically tells that that variable on window should be watched for changes. This means that actual data for results will be stored in window.showSearchApp.searchPage.results .

So assuming, our magical framework works already, this is how we utilize it in our app:

// Global data store var moviesApp = window.moviesApp = { }; // One of the "scopes" to watch var searchPage = moviesApp.searchPage = { }; // Initial array initialization searchPage.results = [ ]; // Listening for user input $("#searchTerm").on('input propertychange paste', _.debounce(queryForShows, 700)); function queryForShows() { var query = $('#searchTerm').val(); if (query) { var data = { query: query, api_key: API_KEY }; $.get( "http://api.themoviedb.org/3/search/movie?" + $.param(data), function(data) { searchPage.results = data.results; }); } else { // clearing search results searchPage.results = []; } }

Note that we don’t do anything with the DOM rendering. When user inputs data we trigger server query and put the results on scope letting the framework do its job and re-render the according views.

So how do we make this work? #

A straight-forward approach would be finding all of the mb-bind elements and render the inner templates for the first time. Then watch the models (scopes), on which they depend and trigger re-rendering each time they change. This is what we do:

$(function () { // Building views-wrappers for every element with mb-bind var views = _.map($('[mb-bind]'), buildView); // Calling initial rendering on each view _.each(views, invoke('render')); // Observing scopes for rendering on each change observeScopes(views); });

Now let’s look how we can implement each of this.

Building views #

This is the easiest part, I guess. We just take an element, wrap it into an object along with its bindings (data it depends on) and its scope .

function buildView(el) { var bindings = $(el).attr('mb-bind').split(','); var scope = $(el).parents('[mb-scope]').attr('mb-scope'); if (!scope) error('Binding is outside of scope (mb-scope is missing)'); return new View($(el), scope, bindings); }

And then view is just an object that we can ask to render itself. It knows what to render, what template to use and where to take data from.

function View($el, scope, bindings) { this.$el = $el; this.scope = scope; this.template = Handlebars.compile($el.html()); this.bindings = bindings; } View.prototype.render = function () { var keyValue = (function (name) { var value = onWindow(this.scope)[name]; return [name, value]; }).bind(this); var data = _.object(_.map(this.bindings, keyValue)); this.$el.html(this.template(data)); };

Observing scopes #

Now if scopes are changed we should try to figure out what exactly changed in them and if there are some views that depend on this data we should render them. Sounds simply, but we can’t just use Object.observe for that, because of its shallow nature (see above). We will implement deepObserve instead and use it to watch with it instead.

function observeScopes(views) { var elements = $('[mb-scope]'); _.each(elements, function (el) { var scopeName = $(el).attr('mb-scope'); var scope = onWindow(scopeName); var onScopeChange = function (changes) { changes.forEach(function (change) { var viewsToRerender = _.filter(views, function(view) { return (_.contains(view.bindings, change.name) && view.scope == scopeName) }); _.each(viewsToRerender, invoke('render')); }); }; deepObserve(scope, onScopeChange); }); }

I’m not gonna put the objectObserve implementation here, cause there’s already a lot of code in this article. But you can check it out by yourself. The main idea is quite simple. We monitor every inner object with Object.observe and fire a callback on the top object.

Wrapping it up #

As you saw Object.observe can be a very powerful thing, and there’s a lot of cool stuff you can do with it without the help of MVC frameworks. Keep in mind that it’s not supported by all browsers yet, but this is just a matter of time, you know.

You can also follow me on twitter

176 Kudos