Rolling Your Own Framework: A Practical Example

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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.

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).

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.

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.

Tags:

Comments

Related Articles