Skip to content

Instantly share code, notes, and snippets.

@patocallaghan
Created February 24, 2014 16:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save patocallaghan/9191146 to your computer and use it in GitHub Desktop.
Save patocallaghan/9191146 to your computer and use it in GitHub Desktop.
Intercom_Views.md

Views

The what and the why?

The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application Justin Meyer, author JavaScriptMVC

To promote modularity and reuseability within our code base you should be building your code to follow the following principles:

  • Don't Repeat Yourself - Similar functionality shouldn't be copied/duplicated in different modules. Abstract it out to a common module.
  • Single Responsibility Principle - Modules should do one thing and one thing well.
  • Highly Cohesive - Should all code in the module be there? Does it contribute to the singular purpose of the module? If some code seems "smelly" and doesn't seem to fit within the goal of your module, maybe it could be abstracted out to another module.
  • Loose Coupling - Modules shouldn't necessarily know about the existence of other modules. If you change the implementation of module A, will it have a knock-on effect and cause the refactoring of Module B. While there will always be a degree of coupling in any system, try to keep it to a minimum.

Views allow us to follow these principles by encapsulating tightly-focused bits of UI into single modules of code. They are based around Backbone Views with some variations. They differ from Backbone views in that we do not have models or templates. We instead are enhancing existing rendered HTML on the page and using views to create our code in a structured hierarchical manner. This helps us build our interfaces from the bottom-up rather than top-down.

Creating our code in this manner makes it more unit-testable, maintainable and reuseable throughout the site.

Creating a view

Currently we have three different types of views. Base views, Behaviour views and Collection views. Base views are single chunks of UI whereas Collection are for creating views which contain lists of repeatable Base views. Both follow the same format for creation.

//Define the View & namespace
IC.define('App.View.ConversationInbox.Card', function(IC, $, _) {

  //Return the view object
  return IC.App.View.Base.extend({
    ... view functionality ...
  });

});

//Instantiate/Use the view elsewhere
var Card = new IC.App.View.ConversationInbox.Card({
    el: this.$('.view_ConversationInbox_Card')
    ... instance view config ...
});

When creating a view append we employ of the convention adding a .view_ViewNamespace HTML class to the root element of the view. This allows you to, when looking at the HTML on the page, easily deduce what type of view it is and what file View file it refers back to. Taking the above example, the view_ConversationInbox_Card class tells us that this is a Card view, that the view is at the namespace IC.App.View.ConversationInbox.Card and that the file for it is at javascripts/views/conversation-inbox/card.js.

IC.App.View.Base

API/Properties

el

Native root DOM element


$el

el wrapped in a jQuery object


initialize(options)new View({options})

Function to set up the view, called when the view is instantiated (using new). Use this function to set custom view properties, initialize child views or merge passed in instance config. Properties set on this will be available throughout the module.

//Define the view
IC.define('App.View.Test', function(IC, $, _){

    ...
    
    initialize: function(options) {
        
        // Set default property
        this.setting = 'foo';

    },
    
    _internal_function: function(){
        console.log(this.setting); // 'foo'
    }
    
    ...
});

//Create instance of the view
new IC.App.View.Test({
    el: '.view_SomeView',
    setting: 'bar'
});

Any options which passed in the options can be added to the this object for use within the whole view. Mixing in the options also allows you to overwrite default configuration settings.

//Define the view
IC.define('App.View.Test', function(IC, $, _){

    ...
    
    initialize: function(options) {
        
        // Set default property
        this.setting = 'foo';
        console.log(this.setting); // 'foo'
        
        //Mixin instance config
        _.extend(this, options);
        console.log(this.setting); // 'bar'
        
    }
    
    ...
});

//Create instance of the view
new IC.App.View.Test({
    el: '.view_SomeView',
    setting: 'bar'
});

events

Convenience configuration object for setting up view events.

Setting an event handler on the view's root element, $el.

'event_type': 'event_handler'

Setting a delegated event handler on child elements of the view.

'event_type selector': 'event_handler'

Example

You callbacks will always run in the context of the view object.

events: {
    'click': '_on_click', //root element
    'click .js_clicked_child': '_on_delegated_click' //child element
},

_on_click: function(){
    console.log(this); //"this" refers to the view
},
_on_delegated_click: function(){
    console.log(this); //"this" refers to the view
}

add_child_view(view_instance) object.add_child_view(view)

Used to insert child views and attach behaviours to the current view. This is important to do because if destroy() is called on the current view, it will first clean up and destroy all child views before destroying itself.

var test_view = this.register_view(new IC.App.View.Test({el: '.view_SomeView'}));
console.log(test_view) //newly instantiated view object

destroy() object.destroy()

Automatically unbinds all events created in the events config, calls the custom created on_destroy() function and removes the view from the DOM.

var test_view = this.register_view(new IC.App.View.Test({el: '.view_SomeView'}));
test_view.destroy();

on_destroy()

If any custom clean up of the View needs to be done add it to the on_destroy function. This will fire automatically when the destroy() function is called.

Destroying child views

destroy() also cleans up all child views, destroying the bottom of the chain first and recursively working its way back up to the intial view that made the destroy() call.

For example, take the following view hierarchy:

Parent View
    - Child View 1
    - Child View 2
        - Child View 3
            - Child View 4
        - Child View 5

Calling destroy() on the Parent View will result in the following destroy order.

Child View 1
Child View 4
Child View 3
Child View 5
Child View 2
Parent View

//Set up View namespace
IC.define('App.View.ConversationInbox.Card', function(IC, $, _) {

  //Return the view object
  return IC.App.View.Base.extend({
    
    //Constructor to init the view instance
    initialize: function (options) {
        _.extend(this, options);
    },
    
    //Convenience method for setting up events for the view
    events: {
        'click': '_on_card_click'
    },
    
    //Custom unbinding of events and cleanup of the view. Fires during the `destroy()` call.
    on_destroy: function(){
    },
    
    //Custom functions
    _on_card_click: function(){
    
    }

  });

});

var Card = new IC.App.View.ConversationInbox.Card({
    el: this.$('.view_ConversationInbox_Card')
});

IC.App.View.Behaviour

Behaviour views are pre-existing modules of re-useable functionality which can be added to your view to enhance it. Examples of Behaviour views include:

  • IC.App.View.Behaviour.Selector - functionality to abstract the selection logic from the user list and the conversation inbox into a common view.
  • IC.App.View.Behaviour.Paging - functionality to implement paging so if you added this to your list you would immediately have infinite scroll functionality.

Behaviour views extend the Base view so every property available to the Base view is also available to a Behaviour view.

Similar to regular views, Behaviour views have their own HTML class name convention. Class names which follow the convention .behaviour_Name should be added to the root element of the view. For example, the Selector behaviour would have the class name .behaviour_Selector.

You can find all the currently implemented behaviours here

IC.App.View.Collection

Collection views are used to generate a list of specified views. They are very similar to Base views as they have the exact same API/properties with some additional features on top. Views to be created will

API/Properties

The exact same API/Properties as the Base view with the following additions. Base view elements should be direct children of the Collection root element.

item_view

A string value denoting what the type of the child view to be created.

item_view: 'IC.App.View.SomeView`

item_view_options

Pass custom configuration properties to the item_view when they are initialised.

Doing the following

...
item_view: 'IC.App.View.SomeView'
item_view_options: {
    test_config: true
}
...

works the same way as if we were to initialise a regular view.

new IC.App.View.SomeView({
    test_config: true
});

append(list_items) object.append(html)

When adding new items to the list, add them to the Collection using the append function. You can pass in either a html string or a jQuery collection. When appended each list item is intialised with the item_view View. append adds new list items to the end of the list.


/*
<ul class="view_Someview">
    <li>content</li>
    <li>content</li>
    <li>content</li>
</ul>
*/

//Init the Collection View
var test_view = new IC.App.View.Collection.SomeView({
    el: '.view_SomeView',
    item_view: 'IC.App.View.TestItem',
    item_view_options: {
        custom_setting: true
    }
});

//Listen to an event which tells us that some new content has loaded
event_bus.on('loaded.content', function(html){

    /*
    <li>content</li>
    <li>content</li>
    <li>content</li>
    */
    
    //Add the new html to the end of the current collection
    test_view.append(html);
});

prepend(list_items) object.prepend(html)

Works exactly the same as append except it adds the new items to the start of the list before any existing list items.

View Communication

To promote loose coupling within our apps we use an eventing system based around the Observer Pattern (also sometimes referred to as Publish/Subscribe or PubSub). Events are used to signify that something has occurred.

API

Creating an event object

The IC.Util.EventAggregator() object is the base Events object in the system. To create an event object we simply create an instance of that.

var object = new IC.Util.EventPropagation();

on object.on(event, callback, [context])

Subscribe to events on an object using the on function.

// Subscribe to the 'replied.thread.conversation_inbox' event
object.on('replied.thread.conversation_inbox', function(){
    console.log('This thread has been replied to!');
});

//Trigger the 'replied.thread.conversation_inbox' event
object.trigger('replied.thread.conversation_inbox');

//=> Logs 'This thread has been replied to!'

When creating namespaces, as a convention, create a period-delimited string working from right to left in order of hierarchy, with the right-most being the more general namespace and the left being the event of the specific module.

For example, replied.thread.conversation_inbox tells us that a replied event has occurred in a thread in the conversation_inbox section of the interface.

off object.off(event, callback, [context])

Unsubscribes from an object event.

// Removes just the `on_change` callback.
object.off("change", on_change);

// Removes all "change" callbacks.
object.off("change");

// Removes the `onChange` callback for all events.
object.off(null, on_change);

// Removes all callbacks for `context` for all events.
object.off(null, null, context);

// Removes all callbacks on `object`.
object.off();

trigger object.trigger(event, [*args])

Triggers an event on a given object. You may also pass in custom parameters to the event.

// Subscribe to the 'replied.thread.conversation_inbox' event
object.on('replied.thread.conversation_inbox', function(data){
    console.log('Hello! ' + data.name);
});

//Trigger the 'replied.thread.conversation_inbox' event
object.trigger('replied.thread.conversation_inbox', [{
    name: 'Pat'
}]);

//=> Logs 'Hello Pat'

If passing custom parameters, as a convention, pass an object literal with specific property names rather than a list of params. This reduces the risk of error due to parameters being out of order or being null/undefined.

//Bad
object.trigger('replied.thread.conversation_inbox', ['a', 'b', 'c', 'd']);

//Good
object.trigger('replied.thread.conversation_inbox', [{
    a: 'a',
    b: 'b',
    c: 'c',
    d: 'd'
}]);

Event Contexts

There are two types of contexts in which the events can operate. At an app-wide level and at the View level.

App-wide events

By default, app-wide events can be subscribed, unsubscribed or triggered on the IC.App.bus object. These are completely decoupled and any module on the page can listen to any event on the app-wide bus.

IC.App.bus.on('added.segment', function(data){
    console.log('A new segment, ' + data.segment_name + ', has been added');
});

IC.App.bus.trigger('added.segment', [{
    segment_name: 'Test Segment',
}]);

// => 'A new segment, Test Segment, has been added'

View-level events

If you need parent-child view interaction you can create an event bus at the parent level and pass the event bus instance to the child view. In the case of deeply-nested views, you can pass the parent's event bus down the view chain as far you need it go.

For example:

//Top-most "Level 1" view
IC.define('App.View.Level1', function(IC, $, _) {

  //Return the view object
  return IC.App.View.Base.extend({
    initialize: function(options){
    
        // Copy "options" properties to "this"
        _.extend(this,options);
        
        //Set up the event bus
        this.level1_bus = new IC.Util.EventAggregator();
        
        //Set up the callback to handle the 'something' event
        this.level1_bus.on('something', function(data){
            console.log('Something happened on Level' + data.level);
        });
        
        //Initialise and pass through the event bus object to the child view
        var level2_view = new IC.App.View.Level2({
            el: this.$('.view_Level2'),
            level1_bus: this.level1_bus
        });
    },
    
  });

});

//Mid-level "Level 2" View
IC.define('App.View.Level2', function(IC, $, _) {

  //Return the view object
  return IC.App.View.Base.extend({
    initialize: function(options){
    
        // Copy "options" properties to "this"
        _.extend(this,options);
        
        //Initialise and pass through the event bus object to the child view
        var level3_view = new IC.App.View.Level3({
            el: this.$('.view_Level3'),
            level1_bus: this.level1_bus
        });
        
        //Trigger the 'something' event on Level 2
        this.level1_bus.trigger('something', [{
            level: '2'
        }]);
        
        // => 'Something happened on Level 2'
    }
  });

});

//Bottom-level "Level 3" View
IC.define('App.View.Level3', function(IC, $, _) {

  //Return the view object
  return IC.App.View.Base.extend({
    initialize: function(config){
    
        // Copy "options" properties to "this"
        _.extend(this,options);
        
        //Trigger the 'something' event on Level 2
        this.level1_bus.trigger('something', [{
            level: '3'
        }]);
        
        // => 'Something happened on Level 3'
    }
  });

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