Backbone, Ember et Angular sont dans un bateau...

Loïc Frering

Backbone, Ember et Angular sont dans un bateau...

Tasks

Backbone

Backbone.js gives structure to web applications

Ember

A framework for creating ambitious web applications

Angular

HTML enhanced for web apps!

Object Model

Object Model

var HandlebarsView = Backbone.View.extend({
  render: function() {
    if (!_.isFunction(this.template)) {
      this.tmpl = Handlebars.compile(this.tmpl);
    }
    this.$el.html(this.tmpl(this.model || this.collection || {}));
    return this;
  }
});

var TaskView = HandlebarsView.extend({
  render: function() {
    console.log('Rendering...');
    TaskView.__super__.render.apply(this);
  }
});

Object Model

var Task = Ember.Object.extend({
  done: false,
  init: function() {
    this.createdAt = new Date();
  },
  createdSince: function() {
    return moment(this.get('createdAt')).fromNow();
  }.property('createdAt')
});

var User = Ember.Object.extend({
  remainingTasks: function() {
    return this.get('tasks').filterProperty('done', false)
                            .get('length');
  }.property('tasks.@each.done')
});
var task1 = Task.create({title: 'Go to Mix-IT'});
task1.set('done', true);
console.log(task1.get('summary'));

var task2 = Task.create({title: 'Enjoy the party!'});
console.log(task2.get('summary'));

var user = User.create();
user.set('tasks', [task1, task2]);
console.log('Remaining: ' + user.get('remainingTasks'));

Services & Dependency Injection


module.factory('Task', ['$resource', function($resource) {
  return $resource('http://localhost\\:3000/tasks/:id', {}, {
    query: {method: 'GET', isArray: false}
  });
}]);

var TasksCtrl = ['$scope', 'Task', function($scope, Task) {
  Task.query({}, function(response) {
    $scope.tasks = response.tasks;
  });
}];

Object Model

MVwhatever

MVwhatever

Model

var Task  = Backbone.Model.extend({
  defaults: {
    title: 'New Task'
  }
});

View

var HandlebarsView = Backbone.View.extend({
  render: function() {
    if (!_.isFunction(this.template)) {
      this.tmpl = Handlebars.compile(this.tmpl);
    }
    this.$el.html(this.tmpl(this.model||this.collection||{}));
    return this;
  }
});

Controller

var TaskFormView = HandlebarsView.extend({
  template: $('#task-form-template').html(),
  events: {
    'submit form': 'save'
  },
  save: function() {
    this.model.save({
      title: this.$('#title').val(),
      description: this.$('#description').val()
    }, {
      success: _.bind(function() {
        Backbone.history.navigate("#task/" +
          this.model.get('id'), true);
      }, this)
    });
    return false;
  }
});

Model

View


<ul>
  {{#each task in model}}
    <li {{bindAttr class="task.active:active"}}>
      <h1>
        {{#linkTo "tasks.show" task}}{{task.title}}{{/linkTo}}
      </h1>
    </li>
  {{/each}}
  <li>{{#linkTo "tasks.new"}}New task...{{/linkTo}}</li>
</ul>

Controller

Model

Scope

View

<ul>
  <li ng-repeat="task in tasks"
      ng-class="{active: task.active}">
    <h1><a href="#/tasks/{{task.id}}">{{task.title}}</a></h1>
  </li>
  <li><a href="#/tasks/new">New task...</a></li>
</ul>

Controller

var TaskDetailsCtrl = ['$scope', '$routeParams', 'Task',
  function($scope, $routeParams, Task) {
    var task = Task.get({id: $routeParams.taskId},
      function(response) {
        $scope.task = response.task;
        Task.active($scope.task);
      }
    );
  }
];
<h1>{{task.title}}</h1>
<p>{{task.description}}</p>

MV*

Binding

Binding

Hello !

Binding

Model Events

Collection Events

var PromptView = Backbone.View.extend({
  events: { keyup: 'change' },
  render: function() {
    this.$el.html('<input id="name" type="text" />');
    return this;
  },
  change: function() {
    this.model.set('name', this.$('#name').val());
  }
});
var HelloView = Backbone.View.extend({
  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
  },
  render: function() {
    this.$el.html('Hello ' + (this.model.get('name') || ''));
    return this;
  }
});

Binding

<ul>
  <script id="metamorph-0-start"></script>
  <script id="metamorph-2-start"></script>
  <li class="active" data-bindattr-1="1">
    <h1>
      <a id="ember378" class="active" href="#/tasks/1">
        <script id="metamorph-4-start"></script>
        Task1
        <script id="metamorph-4-end"></script>
      </a>
    </h1>
  </li>
  <script id="metamorph-2-end"></script>
  <script id="metamorph-3-start"></script>
  <li class="" data-bindattr-2="2">...</li>
  <script id="metamorph-3-end"></script>
  <script id="metamorph-0-end"></script>
  <li>...</li>
</ul>
var AppView = Ember.View.extend({
  template: $('#ember-binding-template').html()
});
<div>{{input value="name"}}</div>
<div>Hello {{name}}!</div>

Binding

Dirty Checking

<div ng-app>
  <div><input type="text" ng-model="name" /></div>
  <div>Hello {{name}}!</div>
</div>

Binding

Routing

Routing & Deep Linking

Associate an URL to a particular application state
Managing Single Page Application layouts?

http://example.com/#/tasks/1
See details of the task with id 1

Routing

var TasksRouter = Backbone.Router.extend({
  routes: {
    '':                'index',
    'tasks':           'list',
    'task/new':        'create',
    'task/:id':        'show',
    'task/:id/edit':   'edit',
    'task/:id/remove': 'remove'
  },

  // ....
});
show: function(id) {
  this.current(TaskDetailsView, id);
}
current: function(viewClass, task) {
  if (!(task instanceof Task)) {
    var id = task;
    task = this.tasks.get(id);
    if (!task) {
      alert('The task with id ' + id + ' does not exist.');
      return this.navigate('#tasks', true);
    }
  }

  this.list();
  this.active(task);

  this.currentView = new viewClass({model: task});
  $('.task').html(this.currentView.render().el);
}
list: function() {
  if (!this.tasksView||this.tasksView.$el.parent().size()===0) {
    this.tasksView = new TasksView({collection: this.tasks});
    $('#app').html(this.tasksView.render().el);
  }

  if (this.currentView) {
    this.currentView.remove();
  }

  this.active();
}

Routing

Tasks.Router.map(function() {
  this.resource('tasks', function() {
    this.route('new', { path: '/new' });
    this.route('show', { path: '/:task_id' });
    this.route('edit', { path: '/:task_id/edit' });
    this.route('remove', { path: '/:task_id/remove' });
  });
});
<div class="tasks">
  <ul>
    {{#each task in model}}
      <li {{bindAttr class="task.active:active"}}>
        <h1>
          {{#linkTo "tasks.show" task}}
            {{task.title}}
          {{/linkTo}}
        </h1>
      </li>
    {{/each}}
    <li>{{#linkTo "tasks.new"}}New task...{{/linkTo}}</li>
  </ul>
</div>
{{outlet}}
Tasks.TasksNewRoute = Ember.Route.extend({
  renderTemplate: function() {
    this.render('tasks/form');
  },
  model: function() {
    return Tasks.Task.createRecord({
      title: 'New Task',
      description: 'Desc'
    });
  },
  setupController: function(controller, task) {
    Tasks.Task.active(task);
    controller.set('content', task);
  },
  events: {
    save: function(task) {
      task.one('didCreate', this, function() {
        this.transitionTo('tasks.show', task);
      });
      this.get('store').commit();
    }
  }
});

Routing

$routeProvider.
when('/tasks', {
  templateUrl: 'partials/tasks.html',
  controller: TasksCtrl
}).
when('/tasks/:taskId', {
  templateUrl: 'partials/tasks.html',
  controller: TasksCtrl,
  nested: {
    templateUrl: 'partials/task-details.html',
    controller: TaskDetailsCtrl
  }
}).
// ...
otherwise({redirectTo: '/tasks'});

Routing

Server API

Server API

Backbone Sync

var Tasks = Backbone.Collection.extend({
  model: Task,
  url:   'http://localhost:3000/tasks',
  parse: function(response) {
    return response.tasks;
  }
});
save: function() {
  this.model.save({
    title: this.$('#title').val(),
    description: this.$('#description').val()
  }, {
    success: _.bind(function() {
      Backbone.history.navigate("#task/" + this.model.get('id'), true);
    }, this)
  });
  return false;
}

Ember Data

save: function(task) {
  task.one('didCreate', this, function() {
    this.transitionTo('tasks.show', task);
  });
  this.get('store').commit();
}

$http & $resource

module.factory('Task', ['$resource', function($resource) {
  var resource = $resource('http://localhost\\:3000/tasks/:id', {}, {
    query: {method: 'GET', isArray: false}
  });
]);

var TaskNewCtrl = ['$scope', '$location', 'Task',
  function($scope, $location, Task) {
    var task = new Task({
      title: 'New Task',
      description: 'Desc'
    });
    $scope.task = task;

    $scope.save = function() {
      this.task.$save(function(t) {
        $location.path('/tasks/' + t.id);
      });
    };
  }
];

Server API

Components

Components

Components

Components


<form {{action save model on="submit"}}>
  <div>
    {{input value="title" placeholder="Title"}}
  </div>
  <div>
    {{textarea value="description" placeholder="Description"}}
  </div>
  ...
</form>

Directives

Components

Testability

Testability

Testability

Testability

Ecosystem

Ecosystem

A backbone

Ecosystem

Ecosystem

Conclusion

Backbone

Ember

Angular

Thanks! Questions?