Skip to content

Instantly share code, notes, and snippets.

@jinjor
Last active December 31, 2015 08:39
Show Gist options
  • Save jinjor/7962143 to your computer and use it in GitHub Desktop.
Save jinjor/7962143 to your computer and use it in GitHub Desktop.

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

tree3

Implementations

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.

tree3

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?

1: God knows everything

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.

2: View works and notifies its parent

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.

3: Subscribes the Model

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.

4: Each View Subscribes the Model

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!

5: Views choose Models

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.

Implementing No.4 using Backbone.js

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.

Angular directives work like No.2

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.

Discussion

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment