In the first part of the series, we talked about components that allow you to manage different behaviors using facets, and how Milo manages messaging.
In this article, we’ll look at another common problem in developing browser applications: The connecting of models to views. We’ll unravel some of the “magic” that makes two-way data binding possible in Milo, and to wrap things up, we’ll build a fully functional To Do application in less than 50 lines of code.
Models (Or Eval Is Not Evil)
There several myths about JavaScript. Many developers believe that eval is evil and should never be used. That belief leads to many developers being unable to say when eval can and should be used.
Mantras like “eval
is evil” can only be damaging when we are dealing with something that is essentially a tool. A tool is only “good” or “bad” when given a context. You wouldn’t say that a hammer is evil, right? It really depends how you use it. When used with a nail and some furniture, “hammer is good”. When used to butter your bread, “hammer is bad”.
While we definitely agree that eval
has its limitations (e.g. performance) and risks (especially if we eval code entered by the user), there are quite a few situations when eval is the only way to achieve desired functionality.
For example, many templating engines use eval
within the scope of with operator (another big no-no among developers) to compile templates to JavaScript functions.
When we were thinking what we wanted from our models, we considered several approaches. One was to have shallow models like Backbone does with messages emitted on model changes. While easy to implement, these models would have limited usefulness – most real life models are deep.
We considered using plain JavaScript objects with the Object.observe
API (which would eliminate the need to implement any models). While our application only needed to work with Chrome, Object.observe
only recently became enabled by default – previously it required turning on Chrome flag, which would have made both deployment and support difficult.
We wanted models that we could connect to views but in such a way that we could change structure of view without changing a single line of code, without changing the structure of the model and without having to explicitly manage the conversion of the view model to the data model.
We also wanted to be able to connect models to each other (see reactive programming) and to subscribe to model changes. Angular implements watches by comparing the states of models and this becomes very inefficient with big, deep models.
After some discussion, we decided that we would implement our model class that would support a simple get/set API to manipulate them and that would allow subscribing to changes within them:
var m = new Model; m('.info.name').set('angular'); console.log(m('.info').get()); // logs: {name: 'angular'} m.on('.info.name', onNameChange); function onNameChange(msg, data) { console.log('Name changed from', data.oldValue, 'to', data.newValue); } m('.info.name').set('milo'); // logs: Name changed from angular to milo console.log(m.get()); // logs: { info: { name: 'milo' } } console.log(m('.info').get()); // logs: { name: 'milo' }
This API looks similar to normal property access and should provide safe deep access to properties – when get
is called on non-existent property paths it returns undefined
, and when set
is called, it creates missing object/array tree as required.
This API was created before it was implemented and the main unknown that we faced was how to create objects that were also callable functions. It turns out that to create a constructor that returns objects that can be called, you have to return this function from constructor and to set its prototype to make it an instance of the Model
class at the same time:
function Model(data) { // modelPath should return a ModelPath object // with methods to get/set model properties, // to subscribe to property changes, etc. var model = function modelPath(path) { return new ModelPath(model, path); } model.__proto__ = Model.prototype; model._data = data; model._messenger = new Messenger(model, Messenger.defaultMethods); return model; } Model.prototype.__proto__ = Model.__proto__;
While the __proto__
property of the object is usually better to be avoided, it is still the only way to change the prototype of the object instance and the constructor prototype.
The instance of ModelPath
that should be returned when model is called (e.g. m('.info.name')
above) presented another implementation challenge. ModelPath
instances should have methods that correctly set properties of models passed to model when it was called (.info.name
in this case). We considered implementing them by simply parsing properties passed as strings whenever those properties are accessed, but we realized that it would have resulted in inefficient performance.
Instead, we decided to implement them in such way that m(‘.info.name’)
, for example, returns an object (an instance of ModelPath
“class”) that has all accessor methods (get
, set
, del
and splice
) synthesized as JavaScript code and converted to JavaScript functions using eval
.
We also made all these synthesized methods cached so once any model used .info.name
all accessor methods for this “property path” are cached and can be reused for any other model.
The first implementation of get method looked like this:
function synthesizeGetter(path, parsedPath) { var getter; var getterCode = 'getter = function value() ' + '{\n var m = ' + modelAccessPrefix + ';\n return '; var modelDataProperty = 'm'; for (var i=0, count = parsedPath.length-1; i < count; i++) { modelDataProperty += parsedPath[i].property; getterCode += modelDataProperty + ' && '; } getterCode += modelDataProperty + parsedPath[count].property + ';\n };'; try { eval(getterCode); } catch (e) { throw ModelError('ModelPath getter error; path: ' + path + ', code: ' + getterCode); } return getter; }
But the set
method looked much worse and was very difficult to follow, to read and to maintain, because the code of the created method was heavily interspersed with the code that generated the method. Because of that, we switched to using the doT templating engine to generate the code for accessor methods.
This was the getter after switching to using templates:
var dotDef = { modelAccessPrefix: 'this._model._data', }; var getterTemplate = 'method = function value() { \ var m = {{# def.modelAccessPrefix }}; \ {{ var modelDataProperty = "m"; }} \ return {{ \ for (var i = 0, count = it.parsedPath.length-1; \ i < count; i++) { \ modelDataProperty+=it.parsedPath[i].property; \ }} {{=modelDataProperty}} && {{ \ } \ }} {{=modelDataProperty}}{{=it.parsedPath[count].property}}; \ }'; var getterSynthesizer = dot.compile(getterTemplate, dotDef); function synthesizeMethod(synthesizer, path, parsedPath) { var method , methodCode = synthesizer({ parsedPath: parsedPath }); try { eval(methodCode); } catch (e) { throw Error('ModelPath method compilation error; path: ' + path + ', code: ' + methodCode); } return method; } function synthesizeGetter(path, parsedPath) { return synthesizeMethod(getterSynthesizer, path, parsedPath); }
This proved to be a good approach. It allowed us to make the code for all of the accessor methods we have (get
, set
, del
and splice
) very modular and maintainable.
The model API we developed proved to be quite usable and performant. It evolved to support array elements syntax, splice
method for arrays (and derived methods, such as push
, pop
, etc.), and property/item access interpolation.
The latter was introduced to avoid synthesizing accessor methods (which is much slower operation that accessing property or item) when the only thing that changes is some property or item index. It would happen if array elements inside the model have to be updated in the loop.
Consider this example:
for (var i = 0; i < 100; i++) { var mPath = m('.list[' + i + '].name'); var name = mPath.get(); mPath.set(capitalize(name)); }
In every iteration, a ModelPath
instance is created to access and update name property of the array element in the model. All instances have different property paths and it will require synthesizing four accessor methods for each of 100 elements using eval
. It will be a considerably slow operation.
With property access interpolation the second line in this example can be changed to:
var mPath = m('.list[$1].name', i);
Not only does it look more readable, it is much faster. While we still create 100 ModelPath
instances in this loop, they all will share the same accessor methods, so instead of 400 we are synthesizing only four methods.
You are welcome to estimate performance difference between these samples.
Reactive Programming
Milo has implemented reactive programming using observable models that emit notifications on themselves whenever any of their properties change. This has allowed us to implement reactive data connections using the following API:
var connector = minder(m1, '<<<->>>', m2('.info')); // creates bi-directional reactive connection // between model m1 and property “.info” of model m2 // with the depth of 2 (properties and sub-properties // of models are connected).
As you can see from above line, ModelPath
returned by m2('.info')
should have the same API as the model, which means that has the same messaging API as the model and also is a function:
var mPath = m('.info); mPath('.name').set(''); // sets poperty '.info.name' in m mPath.on('.name', onNameChange); // same as m('.info.name').on('', onNameChange) // same as m.on('.info.name', onNameChange);
In a similar way, we can connect models to views. The components (see the first part of the series) can have a data facet that serves as an API to manipulate DOM as if it were a model. It has the same API as model and can be used in reactive connections.
So this code, for example, connects a DOM view to a model:
var connector = minder(m, ‘<<<->>>’, comp.data);
It will be demonstrated in more detail below in the sample To-Do application.
How does this connector work? Under the hood, the connector simply subscribes to the changes in the data sources on both sides of the connection and passes the changes received from one data source to another data source. A data source can be a model, model path, data facet of the component, or any other object that implements the same messaging API as model does.
The first implementation of connector was quite simple:
// ds1 and ds2 – connected datasources // mode defines the direction and the depth of connection function Connector(ds1, mode, ds2) { var parsedMode = mode.match(/^(\<*)\-+(\>*)$/); _.extend(this, { ds1: ds1, ds2: ds2, mode: mode, depth1: parsedMode[1].length, depth2: parsedMode[2].length, isOn: false }); this.on(); } _.extendProto(Connector, { on: on, off: off }); function on() { var subscriptionPath = this._subscriptionPath = new Array(this.depth1 || this.depth2).join('*'); var self = this; if (this.depth1) linkDataSource('_link1', '_link2', this.ds1, this.ds2, subscriptionPath); if (this.depth2) linkDataSource('_link2', '_link1', this.ds2, this.ds1, subscriptionPath); this.isOn = true; function linkDataSource(linkName, stopLink, linkToDS, linkedDS, subscriptionPath) { var onData = function onData(path, data) { // prevents endless message loop // for bi-directional connections if (onData.__stopLink) return; var dsPath = linkToDS.path(path); if (dsPath) { self[stopLink].__stopLink = true; dsPath.set(data.newValue); delete self[stopLink].__stopLink } }; linkedDS.on(subscriptionPath, onData); self[linkName] = onData; return onData; } } function off() { var self = this; unlinkDataSource(this.ds1, '_link2'); unlinkDataSource(this.ds2, '_link1'); this.isOn = false; function unlinkDataSource(linkedDS, linkName) { if (self[linkName]) { linkedDS.off(self._subscriptionPath, self[linkName]); delete self[linkName]; } } }
By now, the reactive connections in milo have substantially evolved - they can change data structures, change the data itself, and also perform data validations. This has allowed us to create a very powerful UI/form generator that we plan to make open-source too.
Building a To-Do app
Many of you will be aware of the TodoMVC project: A collection of To-Do app implementations made using a variety of different MV* frameworks. The To-Do app is a perfect test of any framework as it is fairly simple to build and compare, yet requires a fairly broad range of functionality including CRUD (create, read, update and delete) operations, DOM interaction, and view/model binding just to name a few.
At various stages of the development of Milo, we tried to build simple To-Do applications, and without fail, it highlighted framework bugs or shortcomings. Even deep into our main project, when Milo was being used to support a much more complex application, we have found small bugs this way. By now, the framework covers a most areas required for web application development and we find the code required to build the To-Do app to be quite succinct and declarative.
First off, we have the HTML markup. It is a standard HTML boilerplate with a bit of styling to manage checked items. In the body we have an ml-bind
attribute to declare the To-Do list, and this is just a simple component with the list
facet added. If we wanted to have multiple lists, we should probably define a component class for this list.
Inside the list is our sample item, which has been declared using a custom Todo
class. While declaring a class is not necessary, it makes the managing of the component’s children much simpler and modular.
<html> <head> <script src="../../milo.bundle.js"></script> <script src="todo.js"></script> <link rel="stylesheet" type="text/css" href="todo.css"> <style> /* Style for checked items */ .todo-item-checked { color: #888; text-decoration: line-through; } </style> </head> <body> <!-- An HTML input managed by a component with a `data` facet --> <input ml-bind="[data]:newTodo" /> <!-- A button with an `events` facet --> <button ml-bind="[events]:addBtn">Add</button> <h3>To-Do's</h3> <!-- Since we have only one list it makes sense to declare it like this. To manage multiple lists, a list class should be setup like this: ml-bind="MyList:todos" --> <ul ml-bind="[list]:todos"> <!-- A single todo item in the list. Every list requires one child with an item facet. This is basically milo's ng-repeat, except that we manage lists and items separately and you can include any other markup in here that you need. --> <li ml-bind="Todo:todo"> <!-- And each list has the following markup and child components that it manages. --> <input ml-bind="[data]:checked" type="checkbox"> <!-- Notice the `contenteditable`. This works, out-of-the-box with `data` facet to fire off changes to the `minder`. --> <span ml-bind="[data]:text" contenteditable="true"></span> <button ml-bind="[events]:deleteBtn">X</button> </li> </ul> <!-- This component is only to show the contents of the model --> <h3>Model</h3> <div ml-bind="[data]:modelView"></div> </body>
In order for us to run milo.binder()
now, we’ll first need to define the Todo
class. This class will need to have the item
facet, and will basically be responsible for managing the delete button and the checkbox that is found on each Todo
.
Before a component can operate on its children, it needs to first wait for the childrenbound
event to be fired on it. For more information about the component lifecycle, check out the documentation (link to component docs).
// Creating a new facetted component class with the `item` facet. // This would usually be defined in it's own file. // Note: The item facet will `require` in // the `container`, `data` and `dom` facets var Todo = _.createSubclass(milo.Component, 'Todo'); milo.registry.components.add(Todo); // Adding our own custom init method _.extendProto(Todo, { init: Todo$init }); function Todo$init() { // Calling the inherited init method. milo.Component.prototype.init.apply(this, arguments); // Listening for `childrenbound` which is fired after binder // has finished with all children of this component. this.on('childrenbound', function() { // We get the scope (the child components live here) var scope = this.container.scope; // And setup two subscriptions, one to the data of the checkbox // The subscription syntax allows for context to be passed scope.checked.data.on('', { subscriber: checkTodo, context: this }); // and one to the delete button's `click` event. scope.deleteBtn.events.on('click', { subscriber: removeTodo, context: this }); }); // When checkbox changes, we'll set the class of the Todo accordingly function checkTodo(path, data) { this.el.classList.toggle('todo-item-checked', data.newValue); } // To remove the item, we use the `removeItem` method of the `item` facet function removeTodo(eventType, event) { this.item.removeItem(); } }
Now that we have that setup, we can call the binder to attach components to DOM elements, create a new model with two-way connection to the list via its data facet.
// Milo ready function, works like jQuery's ready function. milo(function() { // Call binder on the document. // It attaches components to DOM elements with ml-bind attribute var scope = milo.binder(); // Get access to our components via the scope object var todos = scope.todos // Todos list , newTodo = scope.newTodo // New todo input , addBtn = scope.addBtn // Add button , modelView = scope.modelView; // Where we print out model // Setup our model, this will hold the array of todos var m = new milo.Model; // This subscription will show us the contents of the // model at all times below the todos m.on(/.*/, function showModel(msg, data) { modelView.data.set(JSON.stringify(m.get())); }); // Create a deep two-way bind between our model and the todos list data facet. // The innermost chevrons show connection direction (can also be one way), // the rest define connection depth - 2 levels in this case, to include // the properties of array items. milo.minder(m, '<<<->>>', todos.data); // Subscription to click event of add button addBtn.events.on('click', addTodo); // Click handler of add button function addTodo() { // We package the `newTodo` input up as an object // The property `text` corresponds to the item markup. var itemData = { text: newTodo.data.get() }; // We push that data into the model. // The view will be updated automatically! m.push(itemData); // And finally set the input to blank again. newTodo.data.set(''); } });
This sample is available in jsfiddle.
Conclusion
To-Do sample is very simple and it shows a very small part of Milo's awesome power. Milo has many features not covered in this and the previous articles, including drag and drop, local storage, http and websockets utilities, advanced DOM utilities, etc.
Nowadays milo powers the new CMS of dailymail.co.uk (this CMS has tens of thousands of front-end javascript code and is used to create more than 500 articles every day).
Milo is open source and still in a beta phase, so it is a good time to experiment with it and maybe even contribute. We would love your feedback.
Note that this article was written by both Jason Green and Evgeny Poberezkin.
Comments