Your browser doesn't support the features required by impress.js, so you are presented with a simplified version of this presentation.

For the best experience please use the latest Chrome or Safari browser. Firefox 10 (to be released soon) will also handle it.

Document Cloud

Lecture commentée* des sources

* troll

Pourquoi Backbone est important ?

"Without a backbone you could not stand up at all.

They also protect your spinal cord and nerves from damage and are a direct path to the brain."

Pourquoi DocumentCloud ?

Jeremy Ashkenas
@jashkenas

documentcloud.org

https://github.com/documentcloud/documentcloud

Structuration de Backbone

Source

$(function() {

  window.HomePage = Backbone.View.extend({

    el : document.body,

    events : {
      'click #login_button': 'login'
    },

    initialize : function() { /* */},

    login : function() { /* */}
  });

      

Découper le DOM en plusieurs vues

Par défaut non attaché au DOM.

Mais on peut l'attaché à l'initialisation

        
          el : document.body

          el : '#help ul'
        
      

Découper le DOM en plusieurs vues

Ou plus tard


this.panel.add('search_box', 
  dc.app.searchBox.render().el);

this.panel.add('pagination', 
  dc.app.paginator.el);

this.panel.add('search_toolbar', 
  dc.app.toolbar.render().el);

this.panel.add('document_list', 
  this.documentList.render().el);

this.sidebar.add('entities', 
  this.entityList.render().el);
      
Source

Et s'y tenir



    this.$('.account_list_content')
        .prepend(views);
        
      
Source

Uniformiser les interactions avec l'utilisateur


events : {
  'keydown #search_box':   'maybeSearch',
  'search #search_box':    'search',
  'focus #search_box':     'addFocus',
  'blur #search_box':      'removeFocus',
  'click .cancel_search':  'cancelSearch',
  'click #login_button':   'login'
},
      
Source

Une machine à état non persisté


this.setMode('is', 'open');
this.setMode('not', 'open');
this.setMode('is', 'enabled');
this.setMode('not', 'enabled');
this.setMode('is', 'active');
this.setMode('not', 'active');
      
Source

Pour utiliser le sucre syntaxique de Backbone.

 
window.HomePage = Backbone.View.extend({

  events : {
    'keydown #search_box':   'maybeSearch',
    'search #search_box':    'search',
    'focus #search_box':     'addFocus',
    'blur #search_box':      'removeFocus',
    'click .cancel_search':  'cancelSearch',
    'click #login_button':   'login'
  },

  login : function() { /* ... */ },
  search : function() { /* ... */ },

});
       
Source

Constantes

En haut en majuscule, facile à repérer.


dc.controllers.Searcher = Backbone.Router.extend({

  PAGE_MATCHER  : (/\/p(\d+)$/),

  DOCUMENTS_URL : '/search/documents.json',

  FACETS_URL    : '/search/facets.json',
    
      
Source

Un objet pour découper notre code

 
  initialize : function() {
    this.createSubViews();
    this.renderSubViews();
    dc.app.searcher = new dc.controllers.Searcher;
  },

  createSubViews : function() {
    this.sidebar      = new dc.ui.Sidebar();
    this.panel        = new dc.ui.Panel();
    this.documentList = new dc.ui.DocumentList();
    this.entityList   = new dc.ui.EntityList();
    // ...
  },

  renderSubViews : function() {
    var content   = this.$('#content');
    content.append(this.sidebar.render().el);

     

Pour partager un état

 
  initialize : function() {
    _.bindAll(this, '_loadFacetResults');
  }

  loadFacet : function(facet) {
    dc.ui.spinner.show();
    this.currentSearch = $.get(
      this.FACETS_URL, 
      {q : this.searchBox.value(), facet : facet}, 
      this._loadFacetResults,
      'json');
  },

  _loadFacetResults : function(resp) { ... }
       
Source

Pour faire des factories

 
dc.ui.Dialog = Backbone.View.extend({

  options : {
    title       : "Untitled Dialog",
    text        : null,
  },

  // ...
}

_.extend(dc.ui.Dialog, {

  alert : function(text, options) {
    return new dc.ui.Dialog(_.extend({
      mode      : 'alert',
      title     : null,
      text      : text
    }, options)).render();
  },
  // ...
       
Source

Pour créer des classes filles

 
dc.ui.ProjectDialog = dc.ui.Dialog.extend({

  constructor : function(options) {
    dc.ui.Dialog.call(/* */);
  },

  render : function(noHide) {
    dc.ui.Dialog.prototype.render.call(/* */);

       
Source

Pour découpler son code

 
$ grep -R "tab:" .
./ui/workspace/navigation.js:    if (!(silent === true)) this.trigger('tab:' + name);

./ui/workspace/navigation.js:    this.bind('tab:entities', _.bind(function() {
./ui/workspace/help.js:          dc.app.navigation.bind('tab:help',  _.bind(this.openHelpTab, this));
./ui/documents/upload_dialog.js: dc.app.navigation.bind('tab:documents', _.bind(function(){
./app/searcher.js:               dc.app.navigation.bind('tab:search', this.loadDefault);

       

Un modèle/collection riche en code métier

 
dc.model.Document = Backbone.Model.extend({

  publishAtDate : function() { /* */ },

  formattedPublishAtDate : function() {/* */},



dc.model.DocumentSet = Backbone.Collection.extend({

  model    : dc.model.Document,

  downloadSelectedFullText : function() {/* */},

       
Source

Un modèle pour synchroniser l'état des ressources

 
  // Fetch all of the documents page mentions for a given search query.
  fetchMentions : function(query) {
    $.getJSON(this.url() + '/mentions', {q: query}, _.bind(function(resp) {
      this.set(resp);
    }, this));
  },

  // Tell the server to reprocess the text for this document.
  reprocessText : function(forceOCR) {
    var params = {};
    if (forceOCR) params.ocr = true;
    $.ajax({
      url      : this.url() + '/reprocess_text',
      data     : params,
      type     : 'POST',
      dataType : 'json',
      success  : _.bind(function(resp) {
        this.set({access : dc.access.PENDING});
    }, this)});
  },
       
Source

Ce qui entraine une réactualisation des vues

 
dc.ui.Document = Backbone.View.extend({

  constructor : function(options) {
    // ...
    this.model.bind('change', this._onDocumentChange);
    this.model.bind('change:selected', this._setSelected);
    this.model.bind('focus', this.focus);
    this.model.bind('view:pages', this.viewPages);
    this.model.bind('notes:hide', this.hideNotes);
    this.model.notes.bind('add', this._addNote);
    this.model.notes.bind('reset', this._renderNotes);
    this.model.entities.bind('load', this._renderEntities);
    this.model.pageEntities.bind('reset', this._renderPages);
  }

  _onDocumentChange : function() {
    if (this.model.hasChanged('selected')) return;
    this.render();
  },

       

Un model constitué de sous-model

 
dc.model.Document = Backbone.Model.extend({

  constructor : function(attrs, options) {
    this.notes = new dc.model.NoteSet();
    this.notes.url = function() {
      return '/documents/' + id + '/annotations';
    };
    if (this.get('annotations')) 
       this.notes.reset(this.get('annotations'));
       

Surtout du READ

La structure des fichiers

 
├── application.js
├── app
├── lib
├── model
├── ui
│   ├── accounts
│   ├── common
│   ├── documents
│   ├── metadata
│   ├── organizer
│   ├── search
│   └── workspace
└── vendor
       

Un namespace commun

 
(function() {
  window.dc = {};
  dc.controllers = {};
  dc.model = {};
  dc.app = {};
  dc.ui = {};
})();
       
Source

Augmenté par chaque fichier

 
dc.controllers.Workspace = Backbone.Router.extend({
  // ...
});
       
Source

Contenant les classes et les instances

 
  createSubViews : function() {
    dc.app.paginator  = new dc.ui.Paginator();
    dc.app.navigation = new dc.ui.Navigation();
    dc.app.toolbar    = new dc.ui.Toolbar();
    dc.app.organizer  = new dc.ui.Organizer();
    dc.ui.notifier    = new dc.ui.Notifier();
    dc.ui.tooltip     = new dc.ui.Tooltip();
       
Source

Initialisé à un seul endroit

 
dc.controllers.Workspace = Backbone.Router.extend({
  // ...
  createSubViews : function() {
    dc.app.paginator  = new dc.ui.Paginator();
    dc.app.navigation = new dc.ui.Navigation();
    dc.app.toolbar    = new dc.ui.Toolbar();
    dc.app.organizer  = new dc.ui.Organizer();
    dc.ui.notifier    = new dc.ui.Notifier();
    dc.ui.tooltip     = new dc.ui.Tooltip();
    dc.app.searchBox  = VS.init(this.searchOptions());
    this.sidebar      = new dc.ui.Sidebar();
    this.panel        = new dc.ui.Panel();
    this.documentList = new dc.ui.DocumentList();
    this.entityList   = new dc.ui.EntityList();

    if (!dc.account) return;

    dc.app.uploader   = new dc.ui.UploadDialog();
    dc.app.accounts   = new dc.ui.AccountDialog();
    this.accountBadge = new dc.ui.AccountView({model : Accounts.current(), kind : 'badge'});
  },
       
Source

Afin d'intéragir plus simplement entre module.

 
dc.ui.Document = Backbone.View.extend({

  // ...

  searchAccount : function() {
    dc.app.searcher.addToSearch('account', this.model.get('account_slug'));
  },

  searchOrganization : function() {
    dc.app.searcher.addToSearch('group', this.model.get('organization_slug'));
  },

  searchSource : function() {
    dc.app.searcher.addToSearch('source', this.model.get('source').replace(/"/g, '\\"'));
  },

});
       
Source

Des templates compilés

 
dc.ui.Menu = Backbone.View.extend({

  constructor : function(options) {
    this.content        = $(JST['common/menu'](this.options));
  },

  render : function() {
    $(this.el).html(JST['common/menubutton']({label : this.options.label}));
    this._label = this.$('.label');
    $(document.body).append(this.content);
    return this;
  },
       
 
<div class="wrapper">
  <div class="label">
    <%= label %>
  </div>
</div>
<div class="corner"></div>
       
Source JS JST
From Jammit
Backbone patterns

Des questions ?

Fork me on GitHub