Skip to content

Instantly share code, notes, and snippets.

@iegik
Last active January 27, 2021 07:35
Show Gist options
  • Save iegik/557e7736e02a26f3416d2de67ab57c6a to your computer and use it in GitHub Desktop.
Save iegik/557e7736e02a26f3416d2de67ab57c6a to your computer and use it in GitHub Desktop.
Marionette and Backbone best practices
`childEvents` -> `childViewEvents`
`LayoutView` -> `View`
`ItemView` -> `View`
`onBeforeShow` -> `onRender`
`templateHelpers` -> `templateContext`
`(this|self)\.(\w+).show\(` -> `$1\.showChildView('$2', `

Backbone.Marionette

OOP

instance v.s. class methods

class Car extends Backbone.Model {
  // instance methods
  someMethod(){}
  
  static manufacturersByCountry(){
    // custom logic to query manufacturers
  }
}
var Car = Backbone.Model.extend({
  // instance methods

}, {
  // class methods

  manufacturersByCountry: function(country) {
	// custom logic to query manufacturers
  }
});

Inherance

initialize: function() {
	// my custom stuff
	Backbone.View.prototype.initialize.call(this);
}

private / public / static

var persistAuth = function() {}; // private

var Model = Backbone.Model.extend({
	initialize: function() {       // public
		persistAuth.call(this)
	}
}, {
  triggers: {}                   // static
});

Prototype

define(function (require, exports, module) {
    'use strict';
    
    require('css!css-event-widgets');
    var tplList = require('text!templates');
    var Marionette = require('marionette');

    function constructor (options) {
        var self = this;
        var template = _.template(getChunks(tplList)('#todo'));

        var openWidget = function (e){
            var url = self.model.get('url');
            if (!url) return;
            e.preventDefault();
        };

        var events = function () {
            var obj = {};
            obj['click @ui.listItem'] = openWidget;

            return obj;
        };

        Object.assign(self, {
            ui: {
                listItem: '.js-event-widget-list-item'
            },
            template: template,
            events: events
        });
        Marionette.CollectionView.apply(self, arguments);
    }

    return Marionette.CollectionView.extend({constructor: constructor});
});

React

var UserView = Backbone.View.extend({
  render: function() {
    React.renderComponent(new UserComponent(), this.el);
    return this;
  }
});

Router

var Router = BackboneRouteControl.extend({
  routes: {
    ‘users’:		‘users#index’,
    ‘users/:id’:		‘users#show’,
    ‘users/:id/edit’:	‘users#edit’
  }  
});

var UsersController = function() {
  return {
    index: function() {
	...
    },
    show: function(id) {
	...
    },
    edit: function(id) {
	...
    }    
  };
};

var myRouter = new Router({
  controllers: {
    users: new UsersController()
  }
});

Follow model changes

// WRONG
initialize: function() {
  this.listenTo(this.model, 'change', this.render);
}

// GOOD
modelEvents: {
  'change': 'render'
}

Document fragment

var CollectionView = Backbone.View.extend({
  render: function() {
    var fragment = document.createDocumentFragment();

    _.each(this.collection.models, function(item) {
      var view = new MyView({
        model: item
      });

      fragment.appendChild(view.render().el); // Appending to fragment
    }, this);

    this.$el.html(fragment); // Populating the DOM
  }
});

Note: From Marionette v3.x, Marionette.View replaces Marionette.LayoutView and Marionette.ItemView.

CollectionView in most cases does not need template:

var collectionView = new Marionette.CollectionView({
    childView: new Marionette.View({}),
    collection: new Backbone.Collection([])
});

Deep Models

var setByPath = function (model, path, value) {
  var iter = function (model, prop, key) {
    var next = key + 1;
    if (path.length === next) {
      return model.set(prop, value);
    }
    return iter(model.get(prop), path[next], next);
  }
  return path.reduce(iter, model);
}
ES5
const setByPath = (model, path, value) => {
  const iter = (model, prop, key) => {
    const next = key + 1;
    if (path.length === next) {
      return model.set(prop, value);
    }
    return iter(model.get(prop), path[next], next);
  }
  return path.reduce(iter, model);
}
setByPath(model, 'foo.bar'.split('.'), 5); // model.toJSON() -> {foo: {bar:5}}

Getters in Model

Person =  Backbone.Model.extend({
  get: function (attr) {
    if (typeof this[attr] == 'function') {
      return this[attr]();
    }
    return Backbone.Model.prototype.get.call(this, attr);
  },

  name: function() {
    return this.firstName + " " + this.lastName;
  },

  toJSON: function() {
    var attr = Backbone.Model.prototype.toJSON.call(this);
    attr.name = this.name();
    return attr;
  }
});

Events

Подписаться на все события

const extend = (what, how) => ((fn => function (...args) {how.apply(this,args);fn.apply(this,args);})(what));

// All View triggers
Backbone.View.prototype.delegate = extend(Backbone.View.prototype.delegate, function (eventType, selector, listener) {
  this.$el.on(eventType+'.delegateEvents' + this.cid, selector, () => console.log('[ui event]', `${eventType} ${selector}`, this.el));
  return this;
});

// All View private method triggers
Backbone.View.prototype.trigger = extend(Backbone.View.prototype.trigger, function (eventType, payload) {
  console.log('[trigger]', eventType, payload, this.el);
});

// Subscribe to all Backbone events
Backbone.Events.on('all', function (eventType, payload) {
  console.log('[event]', eventType, payload, this.el);
});

// All View public method triggers
Marionette.View.prototype.triggerMethod = extend(Marionette.View.prototype.triggerMethod, function (eventType, payload) {
  console.log('[triggerMethod]', eventType, payload, this.el);
});

// Turn on debug mode
Backbone.Radio.DEBUG = true;

// Subscribe to all Radio channels
_.forEach(Backbone.Radio._channels, channel => Backbone.Radio.tuneIn(channel.channelName));

// trigger
Backbone.View.prototype.trigger('...')

// triggerMethod
Backbone.View.prototype.trigger('childview:...')
initialize: function (){
    ...
    this.listenTo(this, "bookingEventHeader:incMessage", this._onIncomingMessage);
}
...
_triggerMessage: function (actionName, data) {
    var msg = MnHelper.Message.create(actionName, data);
    this.triggerMethod("bookingEventHeader:message", msg)
},

В чём различие bookingEventHeader:incMessage от bookingEventHeader:message, почему не может быть одного типа сообщения - общего? В компоненте m-booking-event-header очень сильно разбиты события на разные области, хотя всё это просто события, главное, чтобы названия различались. Ещё я заметил сильную связь между компонентами завязанную на событиях - во многих зависимых компонентах названия событий захардкожены.

Т.к. в Backbone есть встроенный механизм событий...

triggers: {
    'click @ui.selectItem': 'ModuleName:action'
},
...
events: {
    'click a': 'showModal'
},
...
// Incomming events
childViewEvents: {
    'SubModuleName:action': 'onModuleNameAction' // onModuleNameAction === ModuleName:action
},
...
onModuleNameAction: function () {} // onModuleNameAction === ModuleName:action

Я предлагаю:

var TRIGGER_SELECT_ITEM = 'click @ui.selectItem';
var TRIGGER_CLICK = 'click a';
var EVENT_SELECT_ITEM = 'ModuleName:action'; // ModuleName:action === ModuleName.onAction
var EVENT_SHOW_MODAL = 'showModal';

var TRIGGER_EVENT_SELECT_ITEM = ModuleName.triggers.EVENT_SELECT_ITEM;
...
// Public methods
// shared events
triggers: function () {
    var e = {};
    e[TRIGGER_SELECT_ITEM] = EVENT_SELECT_ITEM; // TRIGGER_SELECT_ITEM === EVENT_SELECT_ITEM
    return e;
},
...
// local events
events: function () {
    var e = {};
    e[TRIGGER_CLICK] = 'showModal';
    return e;
}
...
childViewEvents: function () { // childViewEvents v3+ === childEvents v2.4.4
    var e = {};
    e[TRIGGER_EVENT_SELECT_ITEM] = 'itemSelected';
    return e;
},
...
// Static methods:
triggers: {
    SELECT_ITEM: TRIGGER_SELECT_ITEM
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment