#How to make Hierarchical GUI Components and Applications (2/3)
Original Posts
http://jinjor-labo.hatenablog.com/entry/2013/12/08/221003
http://jinjor-labo.hatenablog.com/entry/2013/12/10/094825
http://jinjor-labo.hatenablog.com/entry/2013/12/12/225955
##Introduction
This post is after the following post.
How to make Hierarchical GUI Components and Applications https://gist.github.com/jinjor/7952359
The summary of the above is that if a model belongs to multi views as individual data, the cost of synchronizing them is sometimes very high. So we want the model to be shared by views.
A model is to be shared by two views. But each view is not aware of the change of the model by natural, so that if one view updates the model, the other is still keeping the original state. We have to implement the behavior of that when a model is updated, both view immidiately update themselves.
Here is a very simple example. We have two components for each:
- shows members and allows to add new person
- shows the sum amount of their money where a member (and his money) is represented by a number.
When a click event is fired by 'add' button, the list component shows one more value and the sum component updates its number. Then, which following implementation is the best choise?
var list = [];
var view1 = new View1();
var view2 = new View2();
view1.on("add", function(e){
list.add(new Model());
var sum = sumOf(list);
view1.render(list);
view2.render(sum);
});
This may be the simplest. Views work only for rendering and firing events. Models are updated outside and the logic for the next view is also written here. This coding is kind of frustrating because we expects they work well as they are named. We also want them to be encapsulated for changing their details.
var list = [];
var view1 = new View1(list);
var view2 = new View2(list);
view1.on("add", function(e){
view1.render();
view2.render();
});
The second implementation resolves the problem of encapslation. Views updates their models as they are expected, and fire the events on the end. But you have to remember all the views that depend on the target model, which will be annoying when there exist much more views and the model is passed far from where it is declared.
var list = [];
var view1 = new View1(list);
var view2 = new View2(list);
list.on("add", function(e){
view1.render();
view2.render();
});
Now you don't have to remember what can update the model because this version of model fires an event when it is updated. But you still have to know about which view have to render it.
var list = [];
var view1 = new View1(list);
var view2 = new View2(list);
This version of views can subscribe the changes of models. In addition, they can also change the strategy of rendering by event types. Okey, this is the best!
list = [];
var view1 = new View1();
var view2 = new View2();
The last one is a bit extreme. Views can pick up the models from the global area as they like. This, of course, cause several problems.
- You cannot change the model names.
- There is no clue to know the dependency in the code.
Backbone.js provides easy way to implement an observer pattern.
The following is the example. You can see how sumView
begins rendering when listView
updates the members
.
Demo(JSFiddle): http://jsfiddle.net/jinjor/zNnPu/10/
<div id="all">
</div>
var ListView = Backbone.View.extend({
tagName: 'div',
template: _.template('<button class="add">add</button>\
<ul>\
<% list.each(function(value) { %>\
<li><%= value.get("value") %></li>\
<% }); %>\
</ul>\
'),
initialize: function(options){
this.list = options.list;
},
events: {
"click .add": "add"
},
add: function(){
var value = this.list.length + 1;
this.list.add(new Member({value: value}));//リスト更新
this.render();
},
render: function(){
var $el = this.$el.empty();
return $el.html(this.template({
list: this.list
}));
}
});
var SumView = Backbone.View.extend({
tagName: 'div',
template: _.template('<div>sum: <%= sum %></div>'),
initialize: function(options){
this.list = options.list;
this.listenTo(this.list, 'add', this.render);//リスナ登録
},
render: function(){
var $el = this.$el.empty();
var sum = this.list.reduce(function(memo, value){
return memo + value.get('value');
}, 0);
return $el.html($(this.template({
sum: sum
})));
}
});
var Member = Backbone.Model.extend({});
var members = new Backbone.Collection();
members.add(new Member({value: 1}));
members.add(new Member({value: 2}));
members.add(new Member({value: 3}));
$('#all')
.append(new ListView({list: members}).render())
.append(new SumView({list: members}).render());
Does anyone know the simpler way to write Member
Model with Backbone?
Anyway, we got them to work well.
AngularJS's model is not observable because they are pure objects. Instead, the Angular system automatically rerenders the model.
That behavior is implemented like No.2, but you don't have to write render()
thanks to the framework.
Now you can see that when <list>
updates the members, <sum>
is also rendered.
Demo(JSFiddle): http://jsfiddle.net/WR5Pd/1/
<div ng-app="app">
<div ng-controller="Ctrl">
<list values="members"></list>
<sum values="members"></sum>
</div>
</div>
angular.module('app', [])
.directive('list', function() {
return {
restrict: 'E',
template: '<div><button ng-click="add()">add</button>\
<ul><li ng-repeat="v in values">{{v}}</li></ul></div>',
scope: { values: '=' },// 引数
link: function($scope) {
$scope.add = function(){
var value = $scope.values.length + 1;
$scope.values.push(value);// リストを更新
}
}
};
})
.directive('sum', function() {
return {
restrict: 'E',
template: '<div>sum: {{sum()}}</div>',
scope: { values: '=' },
link: function($scope) {
$scope.sum = function(){
return $scope.values.reduce(function(memo, v){
return memo + v;
},0);
};
}
};
})
.controller('Ctrl', function($scope) {
$scope.members = [1, 2, 3];
});
In this example, the views do not subscribe the models at all, but the whole behavior is the same as previous one.
This post does not aim to figure out the detail of concreate frameworks. Ignoring the performance issues(for my lazy research), the better description of views, I think, is AngularJS's one. Because developers really want custom elements! (don't you?)
Next: Some Technics