2-Way Data Binding under the Microscope
2-way data binding has entrenched modern front-end development as a tool allowing you to avoid boilerplate code when working with DOM, concentrate on logic and isolate the logic from your templates. All the Angular is built around this piece of technology. Pretty big part of Ember joins that. And for the Backbone framework there are numerous extensions appearing every day. It makes a lot of sense actually: 2-way data binding is pretty awesome. However the technology has its own issues, limitations and mostly important implementation details (within different frameworks). Why not speculate about it a little bit?
What is 2-way data binding after all?
JavaScript allows you to build interaction with a user responding to his actions with visual events. User enters some data into a form, presses “submit” button and a page shows loading indicator. Then (given the input contained some kind of error) invalidated fields get highlighted.
What’s happening behind the scenes?
- values of fields get stored into a variable
- variable gets serialized into JSON and gets sent onto server with an AJAX query
- DOM gets modified: loading indicator appears
- as soon as the request finishes we see that status is not 200, then we parse response body
- DOM gets modified again: loading indicator disappears, invalidated fields get highlighted
Classical jQuery code could work similarly to this:
- a function gets attached to the ‘click’ event of button
- the functon collects fields and puts them into a variable
- the variables gets serialized into JSON and goes to server
- we mark “request in process” state using another variable (to not react double clicks mainly)
- we modify DOM adding indicator
- as soon as the request finihes we parse response body and get invalidation data
- we modify DOM adding invalidation information and removing the loading indicator
Note that step 7 has huge risk of the encapsulation violation: we modify our view heavily. Where should this logic be? How to avoid duplication of code with something that generated the form initially? Basically it’s the most popular place for projects to turn into mess.
2-way data binding lets us get rid of steps 2, 5 and 7. It also solves the issue with the encapsulation or – as it’s trendy to think now – alows you to completely remove logic from your views.
Given we have a variable called entity
holding JavaScript object. Each field of the form is associated with the attribute of the object (i.e. <input name='name'>
with entity.name
). At the same time the variable can include inlined object entity.errors
containing invalidations list (it’s empty by default). Then if we want to invalidate field entity.name
we are doing something similar to entity.errors.name = 'The field is too short'
. Additionally we set entity.loading
to true
when our AJAX is in progress.
To turn such object into required form we could use a template looking like this (Underscore Template notation):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Now if any change of form input automatically got delivered to entity.name
and vice versa any change of any attribute at entity
caused automatic refresh of DOM according to template definition – that would be the 2-way data binding. Your template completely describes the display logic corresponding to possible states. The application in its turn simply changes states.
All we have to do now is to bind form submission to button click event and work with entity
variable. When the server responds – we put all the validation errors into entity.errors
. That’s it. Much easier isn’t it?
Actually no. It is not.
Unfortunately it is just an example. In real-life it misses a vast amount of meta-inforation that it simply can’t work without. To make it work universally we have to solve the following problems:
- we have to find a way to monitor
entity
modifications recursively. You remember it’s still plain old JavaScript right? - we can’t monitor all the modifications for all the variables and constantly redraw the whole DOM of the page. In best case it will end up being extremely slow. Otherwise it can lead to the loss of state of DOM parts. DOM should be modified as atomically as possible.
- even if we section our page in zones, what’s going to happen if we need to replace the part of text content not touching its siblings? Or how do you update (including the tag itself) two
<tr>
s out of 10? - sometimes we might want to animate automatic modifications instead of changing DOM instantly
- we don’t currently have the reflection of
input
toentity.name
. We imagined having that but how do we actually implement that? Where is this logic supposed to be situated? At the application code withbind
or at the view itself where the reverse binding is described?
To solve all that every framework offers its own unique solutions that add fly in the ointment to a beautiful theory. So let’s dive in and see how 2-way data binding actually works internally and where do all those weird limitations come from?
We are going to dig into 3 examples: Angular as a canonical sample of “new and better HTML”, Ember as a sample of more classical JS paradigm attached to the new tool. And certainly Joosy as a demonstration of my subjective vision of experimental 2-way data binding that doesn’t require declarative templating language.
Object modifications monitoring
Sadly there are no good univeral ways to track objects modifications in JavaScript. All the existing solutions impose some restrictions on how you work with the object. There actually exactly two solutions: wrapping with setters/getters and external monitoring.
Setters/Getters (Ember, Joosy)
Properties working through getters and setters are the classic of programming. All the required data types get wrapped into extending classes (types) definining two methods: get
and set
(or two methods per property). Every call to the set
method makes object generate “something has changed” event. The solution is very simple technically and for obvious reasons quite efficient. However instead of entity.field = 'value'
you have to write entity.set('field', 'value')
. The latter is not only less readable by a human being but also can affect some basic tools like JavaScript Lint and code highlighting.
Ember
Getters and setters are the root of Ember’s properties system. Not only they allow to monitor object modifications but also allow you to subscribe to the modifications of particular fields. Basically it looks exactly as described:
1 2 |
|
On the other hand when we get to arrays (that Ember generalizes into Enumerable
) everything gets a little bit more complicated and confusing. Now we have to additionally watch the length of array and also provide the ability to monitor changes of particular fied of every entry (Ember allows doing that using meta property called @each
: for instance @each.field
to monitor change of field
for every array entry).
If you watch the history of how frameworks evolve you will see one funny repeating loop. Complicated frameworks a-la Rails appear, take over the market and then get split into separate independent components. Components that can be used separately. That is often used to build variety of similar frameworks based on similar technologies. Watchable objects library could be the perfect candidate to become an independent library with a separate API.
Joosy
Getters and setters for objects work identically in Joosy. Excluding the fact that Joosy doesn’t have inline fields monitoring. That’s why arrays work simpler:
1 2 3 |
|
Besides that Joosy mimics Ruby allowing you to additionaly define direct accessors for properties.
1 2 |
|
Such definition will force Joosy to create JavaScrpipt object properties with defineProperty
allowing you to work with them as if they were normal attributes: entity.field1
. Therefore you can get back to entity.field1 = 'value'
instead of entity.set('field1', 'value')
– these are now equal. With this you have to manually define the list of fields, yes, but you get your proper JavaScript syntax back.
External monitoring (Angular)
Angular took its own approch. Instead of attempts to catch attributes modifications from the internals of the object it introduces the global monitoring system. The famous $watch
method that is overabused in every second Angular application.
Angular architecture introduces “drawing cycle”. One of its step is the check against the list of watchers if there are any changes. Every element of the list can refer to one or more functions that are called upon every change. Such solition allows you to transparently work with any way of attributes modifications (no need to know the list of fields or use the set
method). This however is not a silver bullet as well.
Performance
If you used to work with Angular you probably noticed that after the particular amount of active
$watch
assignments everthing slows down critically. The more mobile your client is (and we are living in the mobile technologies epoch) the faster you get to this amount. Performance is the first payment for universalism.Non-scalars
By default the modification check is being held by reference. It means that no matter how you change your array or object Angular is not going to see the change. One of the solutions to this problem is additional mode of
$watch
that allows you to force value-based comparison. It however is even slower. The other problem is that it still might not always work with complicated structures. On the other hand it’s not a normal case when you need to monitor something like that in Angular: its architecture tries to avoid the need to monitor non-scalar data as much as possible.
Protip: try to search for angular watch
at StackOverflow.com to understand the scale of the disaster.
Page sectioning
Now that we can catch objects modifications the time has come to decide how we segment the page. What exactly are we going to update when object changes? Clearly if we output entity.name
field for instance – we only have to update that value and only when it’s changed. What if we output a table in cycle though?
Declarative templates (Angular, Ember)
This is one of the reasons making Angular to have “the other HTML” and making Ebmer to have Handlebars. Declarative description that is being parsed by their own internal parsers provides information about contexts of the bindings.
When we output {{person.first_name}}
, Ember creates a region and attaches that to the updae of first_name
field from the person
object. Angular works similarly: <ng_repeat ...
creates a common region for the whole array and additionaly one inner region per each array element. If the array changes – the bigger region redraws. If one of the elements changes – the inner region redraws.
The conditional declarations (ng_if
and {{if}}
) work in the same way. As soon as value is changed all the region redraws].
This by the way is one of the reasons that makes Angular recomdend to prefer
ng_show
hiding the region instead of rerendering it.
Devise of this approach is: “templates should not contain logic!”. This however looks more like a consequence than a rule. Integration of full logical language into such declarative notation would be extremely expensive. So the wolves are fed and the sheep kept safe. We don’t need language and we achieve the ultimate theoretical goal. Isn’t that great?
Reality is tough though. Logic can’t just disappear, it has to move somewhere. If it’s not at template in any way – it should be defined somewhere as a state. It means that every little feature will add one more virtual state to your application. Loading indicators, accessibility rules, selection marks, each tiny switcher. The understanding of how serious this tax can be can come too late since you normally add tiny visual features at the very end of the project when you can’t just switch the approach.
Manual sectioning (Joosy)
I always wanted to keep my beloved HAML (mixed with CoffeeScript) and at the same time to keep all the abilities of 2-way data binding. To achieve that Joosy implements manual sectioning. Instead of declarative definitions we use classical helpers. One of them for instance allows you to define dynamic region and pass it the local data that this region will watch for.
E.g. to get the behavior similar to ng_repeat
of Angular or to each
of Ember you can do something like this:
1 2 3 4 |
|
As soon as the @projects
array or any of its elements change the modification immediately applies to DOM. Note that region watchers expressly implemented to monitor collections with all the nested values. That’s why you only need one segment in this case.
Besides inlining Joosy allows you to render a partial (just like in Rails) as a region. This case is even more common.
This approach gives you ability to work with any templating language you want. Any notation is fine (Joosy currently supports any templating language compilable by JST). The other benefit is ability to control rendering manually (for instance you can bind a region to the resource that is not outputed explicitly) which might sound destructive but can be useful rarely.
The cons of this approach are the flip side of pros. Not only you can control everything – you actually have to. No regions defined – no 2-way data binding. The other problem is the case with huge regions (1000 rows per table). Since Joosy registeres just one region per array, any modification will alway redraw the whole table. Even worse – you can’t really define a region depending on a particular field. It only accepts a whole resource. This can be painfull when it comes to complicated forms.
Partial DOM updates
Now that we have sections with binding to proper local sets of objects that redraw automatically. Life is getting better. There’s however a new problem to solve:
1 2 3 |
|
What if our region is not entirely a tag content and it can’t be modified with the usage of .innerHTML
?
Metamorph (Ember, Joosy)
Ember and Joosy are on the same page here. We wrote separate solution initialy but ended up using the library Metamorph created by Ember team. And Metamorph works pretty well.
When working in modern browsers Metamorph relies on W3C Ranges. For the old browsers everything gets much more interesting. Region gets wrapped into two script
tags that, according to specification, can be part of any other tags. To update content Metamorph replaces the content between these two tags.
Angular
Since Angular’s approach is based on the “better HTML” – it’s task is a little bit different. In most cases we simply bind to a particular tag so Angular basicly doesn’t have this problem. The only similar case it has is the interpolation. During interpolation Angular binds everything to the closest parent tag and turns it into a region. Thus Angular alway modifies only tags and attributes of tags. No magic.
Another interesting approach
There’s one more interesting approach that I saw implemented by tiny frameworks (mostly implementing just the binding itself). This approach was also mentioned by Yehuda Katz to become a replacement for Metamorph. Instead of interpreting HTML as a text it makes you to parse it and convert it into direct DOM statements:
1 2 3 4 5 6 7 8 |
|
Animations
CSS-classes (Angular)
Since Angular always works on a tag level – it always has a container to interact with. This is a huge advantage when it comes to animations. During the modificaiton of DOM Angular sets special CSS classes to the changing tag. This allows you to easily add CSS-based transitions and animations with no additional code.
Angular also contains additional tools allowing you to bind JS animations to particular CSS classes. Seriously, Angular is amazing when it comes to animations. Some limitations in other areas seriously simplify everything here.
Numerous issues (Ember, Joosy)
Metamorph is on the other page. Everything is much more sad – our regions are not bound to DOM in any way and it’s unclear what should be animated during modification. There are numerous proposals for Ember syntax like {{if flag transition='fade'}}
. However they don’t look realistic with the current implementation of Handlebars. You simply can’t animate random DOM region having no single root.
You could probably explicitly pass the ID of element to animate as addition to the animation style. This however seriously breaks conventions of Ember declarative language. I’m really excited to monitor all the discussions regarding this topic but it doesn’t look like it’s on the priority list of Ember right now.
Joosy is in the similar situation here. It has a little advantage with the fact we work with JS code straightly (CoffeScript code to be more correct). That’s why it’s not limited to strings and can pass JS function directly. So basically we can hook into the rendering internals.
1
|
|
I don’t think it’s a proper solution as well but at least it works. We are currently considering allowing you to pass in the selector and animation keyword as described before:
1
|
|
Reverse binding (inputs to fields of objects)
The last step to bright future is the reverse binding. Automatically changing the fields of objects during the modifications of form fields. Having all our experience it’s a piece of cake. Angular and Ember having declarative templates simply add another attribute specifying which field should be mapped:
1 2 |
|
Joosy implements the same with set of helpers:
1 2 |
|
Finally we got the 2-way binding working!
Instead of conclusion
As you can see there are numerous ways to implement 2-way data binding. While declarative languages (including separate languages like Handlebars and HTML extensions) masterfully dominate, there are other options as well. None of them works perfectly though. Generally this is what all the “modern” tools go through and I’m sure we will come up with more elegant ways of solving the described issues. We are still waiting for tons of exciting HTML features to get accessible and even more to be defined (like Node.bind()).
Be careful until we there though. 2-way data binding is an amazing tool. But the drawbacks can be essential. Make sure to discover them as soon as possible: hopefully you just got one step closer to this.