Tutorial: Knockback Todos App

Try the demo or Look at the Code

Todos Architecture

Knockback follows the MVVM Pattern

With the MVVM pattern, instead of Model, View, Controller you use Model, View, ViewModel. As an simple approximation using MVC terminology:

Knockback ViewModel Injection

Like AngularJS, Knockback.js provides a mechanism through the kb-inject attribute or the data-bind inject Knockback custom binding to bind HTML Views without a central application control point.

<section id="todoapp" kb-inject="AppViewModel">
  <header id="header">
  ...
</section>

Please look at the API Documentation or the ViewModel Injection Tutorial for more information and examples.

MVVM in "Todos - Classic"

The Classic application is an upgraded port of the Backbone Todos application so it has the same ORM with Todo (Backbone.Model) and TodoCollection (Backbone.Collection), but the application's Views are controlled by two ViewModels (AppViewModel and TodoViewModel).

Models (Backbone.Model + Backbone.Collection)

ViewModels

MVVM in "Todos - Extended"

This application extends the "Todos - Classic" by adding settings including todo priorities (display colors and orders), language selection and localized text, adding todos list sorting options (by name, created date, and priority). Along with the following changes:

Models (Backbone.Model + Backbone.Collection)

ViewModels

Localization

Localization is key for the global applications we create today. It should not be an afterthought!

Knockback does not provide a locale manager (although there is a sample implementation with this todos application in: models/locale_manager.coffee) because different applications will retrieve their localized strings in different ways. Instead, Knockback provides a localization pattern by using a simpified Backbone.Model-like signature that hooks into Knockback like any other model:

@trigger('change', @)
@trigger("change:#{key}", value) for key, value of @translations_by_locale[@locale_identifier]
this.trigger('change', @);
for (var key in this.translations_by_locale[this.locale_identifier]) {
  this.trigger("change:#{key}", this.translations_by_locale[this.locale_identifier][key]);
}

Register your custom locale manager like:

kb.locale_manager = new MyLocaleManager()
kb.locale_manager = new MyLocaleManager();

Also, if you want to perform some specialized formatting above and beyond a string lookup, you can provide custom localizer classes derived from kb.LocalizedObservable:

class LongDateLocalizer extends kb.LocalizedObservable
  constructor: -> return super
  read: (value) ->
    return Globalize.format(value, Globalize.cultures[kb.locale_manager.getLocale()].calendars.standard.patterns.f, kb.locale_manager.getLocale())
  write: (localized_string, value, observable) ->
    new_value = Globalize.parseDate(localized_string, Globalize.cultures[kb.locale_manager.getLocale()].calendars.standard.patterns.d, kb.locale_manager.getLocale())
    value.setTime(new_value.valueOf())
var LongDateLocalizer =  kb.LocalizedObservable.extend({
  constructor: function LongDateLocalizer() {
    return LongDateLocalizer.__super__.constructor.apply(this, arguments);
  },

  read: function(value) {
    return Globalize.format(value, Globalize.cultures[kb.locale_manager.getLocale()].calendars.standard.patterns.f, kb.locale_manager.getLocale());
  },

  write: function(localized_string, value, observable) {
    var new_value;
    new_value = Globalize.parseDate(localized_string, Globalize.cultures[kb.locale_manager.getLocale()].calendars.standard.patterns.d, kb.locale_manager.getLocale());
    return value.setTime(new_value.valueOf());
  }
});

Note: kb.LocalizedObservable's constructor actually returns a ko.computed (not the instance itself) so you either need to return super result or if you have custom initialization, return the underlying observable using the following helper: 'kb.wrappedObservable(this)'

You can simply watch an attribute on the locale manager as follows:

AppViewModel = ->
  ...
  @input_placeholder_text = kb.observable(kb.locale_manager, {key: 'placeholder_create'})
var AppViewModel = function() {
  ...
  this.input_placeholder_text = kb.observable(kb.locale_manager, {key: 'placeholder_create'});
};

Or model attributes can be localized automatically when your locale manager triggers a change:

TodoViewModel = (model) ->
  ...
  @completed = kb.observable(model, {key: 'completed', localizer: LongDateLocalizer})
var TodoViewModel = function(model) {
  ...
  this.completed = kb.observable(model, {key: 'completed', localizer: LongDateLocalizer});
};

Lazy Loading

By using Knockback with Backbone.ModelRef, you can start rendering your views before the models are loaded.

As demonstration, you can see that the colors arrive a little after the rendering. It is achieved by passing model references instead of models to the settings view model:

SettingsViewModel = (priorities) ->
  @priorities = ko.observableArray(_.map(priorities, (model)-> return new PrioritiesViewModel(model)))
  ...
window.app.viewmodels.settings = new SettingsViewModel([
  new Backbone.ModelRef(priorities, 'high'),
  new Backbone.ModelRef(priorities, 'medium'),
  new Backbone.ModelRef(priorities, 'low')
])
var SettingsViewModel = function(priorities) {
  this.priorities = ko.observableArray(_.map(priorities, (model)-> return new PrioritiesViewModel(model)));
  ...
window.app.viewmodels.settings = new SettingsViewModel([
  new Backbone.ModelRef(priorities, 'high'),
  new Backbone.ModelRef(priorities, 'medium'),
  new Backbone.ModelRef(priorities, 'low')
]);

and then lazy fetching them (which creates them if they don't exist):

# Load the prioties late to show the dynamic nature of Knockback with Backbone.ModelRef
_.delay((->
  priorities.fetch(
    success: (collection) ->
      collection.create({id:'high', color:'#c00020'}) if not collection.get('high')
      collection.create({id:'medium', color:'#c08040'}) if not collection.get('medium')
      collection.create({id:'low', color:'#00ff60'}) if not collection.get('low')
  )
  ...
), 1000)
// Load the prioties late to show the dynamic nature of Knockback with Backbone.ModelRef
_.delay(function() {
  priorities.fetch({
    success: function(collection) {
      if (!collection.get('high')) { collection.create({id:'high', color:'#c00020'}); }
      if (!collection.get('medium')) { collection.create({id:'medium', color:'#c08040'}); }
      if (!collection.get('low')) { collection.create({id:'low', color:'#00ff60'}); }
    }
  });
  ...
}), 1000);