domingo, 27 de novembro de 2016

Tutorial: a contact manager application with MarionetteJS v3

Warning: opinions ahead


Marionette and Backbone are libraries that do not force an specific application structure, letting for the developer pick the one that better work for him. I've been playing with Marionette and Backbone for a while using several approaches. From using vanilla Backbone with Bootstrap (a good example of how not build an application) to using Marionette with Ionic CSS.

As result of this journey i settled with opinionated approaches for both data binding and routing. For data binding i'm using rivets and for routing a customized router built on top of cherrytree. While the rivets specific code can be easily replaced by, e.g., an traditional templating solution like Handlebars, the application structure used in this tutorial relies on specific router features making undoable to replace it.

This tutorial assumes the reader knows ES6 constructs as import statements, Promise, arrow functions and also is familiar with Backbone.

What?


We will implement the same application as the one built in this Aurelia tutorial. It will use the same assets, but will not to try to follow / reuse the architecture or code. Requires a browser with native Promise implementation.

The architecture will resemble, loosely, the used in an Ember tutorial. Something like data down, events up.

The source code can be found at github.

Setting the environment



npm init

//Webpack (v1) with Babel and Html loaders
npm install webpack html-loader babel-loader babel-core babel-preset-es2015 --save-dev

//Marionette and its dependencies
npm install jquery underscore backbone backbone.radio backbone.marionette --save

//Bootstrap
npm install bootstrap --save

//The routing library
npm install marionette.routing --save

//Rivets 
npm install rivets rivets-backbone-adapter --save

//in package.json:

 "scripts": {
    "build": "webpack",
    "watch": "webpack --watch"
  }

Download the assets and extract into directory src

Create a simple index.html into root folder, only with a div and script tags:



<div id="app"></div>
<script src="build/bundle.js"></script>

Configuring webpack


Directly to the webpack.config.js:

var path = require('path');

module.exports = {
  entry: __dirname + '/src/main.js',
  output: {
    path: __dirname + '/build',
    filename: "bundle.js"
  },
  devtool: "source-map",
  resolve: {
    alias: {
      marionette: 'backbone.marionette'
    }
  },
  module: {
    loaders: [
      {test: /\.html$/, loader: 'html'},
      {
        test: /\.js$/, loader: 'babel?presets[]=es2015', include: [path.resolve(__dirname, 'src')]
      }
    ]
  }
};


Pretty standard stuff: use src/main.js as entry, dist/bundle.js as output with a source map. Use Html loader for *.html files and Babel loader for *.js files inside src using es2015 preset

Configuring the route map


An important step is to configure the routes at the application start.

Here is the code of main.js:

import { createRouter, middleware} from 'marionette.routing';

let router = createRouter({log: true, logError: true});

router.map(function (route) {
  route('application', {path: '/', abstract: true}, function () {
    route('contacts', {}, function () {
      route('contactdetail', {path: ':contactid'})
    })
  })
});

router.use(middleware);

router.listen();


The createRouter is used to, heh..., create the router with the options to log transitions and errors to console.

Three routes are configured: application, the root one, contacts and the leaf contactid. The route nesting means that to a route be active all parents should also be active. So if contacts is active, we can assume that application route is also active, but we cannot know before hand if contactdetails is active.

The path is constructed using route owns path appended to the parent one. When the path is omitted, the name is used. So contactdetails will match the following URL pattern /contacts/:contactid.

Note also the abstract option passed to application route. It means that is not supposed to be navigated to. More info in cherrytree docs

The middleware is the bridge between cherrytree and Marionette Routing interfaces.

Calling listen, the router starts to monitor URL changes and does the first transition using the current location.

Time for the first build. Type npm run build and launch index.html. In the developer tools (F12) console,  the transition info should be logged. Update the URL hash to index.html#contacts. An error message ("Error: Unable to create route instance: routeClass or viewClass must be defined") should appear. Let's fix it.

Meeting the Route class


The Route class provides properties to define declaratively a view to be rendered as well mechanisms to do messaging with the view or other routes using the patterns promoted by Marionette. It acts as a logical point in the application structure and can be used to, among other things, fetch and cache data. In fact, its interface is mostly routing agnostic and could be used decoupled from the router.

Create application/route.js (ApplicationRoute) file and put:

import {Route} from 'marionette.routing';
import {WebAPI} from '../web-api';

export default Route.extend({
  activate(){
    this.api = this.api || new WebAPI();
  },

  channelName: 'api',

  radioRequests: {
    'getContactList': function () {
      return this.api.getContactList()
    }
  }
})

The activate method is called each time a route becomes active i.e. when the URL matches the route path pattern. Since we configured the application route path to '/' and all other routes are child of it, this means that it always be active. Such routes are useful for the setup of data or services required application wide.

The ApplicationRoute creates an api instance in activate and make its interface public through a Radio message system. The channelName and radioRequests properties are inherited from Marionette.Object. Basically it says to reply with a call to api.getContactList the request for 'getContactList' in 'api' channel. More info here.

Now the contacts/route.js (ContactsRoute) file:


import {Route} from 'marionette.routing';
import Radio from 'backbone.radio';
import {Contacts} from '../entities';

export default Route.extend({
  activate(){
    let contactsPromise = Radio.channel('api').request('getContactList');
    return contactsPromise.then(contactsData => {
      this.contacts = new Contacts(contactsData)
    })
  }
})


Since the activate methods are called sequentially, from parent to children routes, we can safely use the Radio 'api' channel configured in the ApplicationRoute. The 'getContactList' request returns a promise to which is added a resolve handler that creates and caches the contacts collection.

Another important characteristic of activate is that will resolve the return value as a promise and will only proceeds for the next route when is resolved. If the returned promise is rejected the transition is canceled.

Now update the route definitions in main.js to use its corresponding classes:


import ApplicationRoute from './application/route';
import ContactsRoute from './contacts/route';

[..]

router.map(function (route) {
  route('application', {path: '/', abstract: true, routeClass: ApplicationRoute}, function () {
    route('contacts', {routeClass: ContactsRoute}, function () {
      route('contactdetail', {path: ':contactid'})
    })
  })
});

Rendering the contacts view


The view layout consists of the contact list in the left and the contact detail on right.

Put the markup below in contacts/template.html

<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
    <div class="navbar-header">
        <a class="navbar-brand" href="#">
            Contacts
        </a>
    </div>
</nav>

<div class="container">
    <div class="row">
        <div class="col-md-4">
            <div class="contact-list">

            </div>
        </div>
        <div class="col-md-8 contact-outlet"></div>
    </div>
</div>

Note the div elements with contact-list and contact-outlet classes that will be used as regions to hold the actual views.

Below is the contact list view implementation in contacts/view.js:


import Mn from 'backbone.marionette';
import DataBinding from '../databinding';

const itemHtml = `          
        <a href="#">
          <h4 class="list-group-item-heading">{model:firstName} {model:lastName}</h4>
          <p class="list-group-item-text">{model:email}</p>
        </a>`;

const ContactItemView = Mn.View.extend({
  behaviors: [DataBinding],
  tagName: 'li',
  className: 'list-group-item',
  html: itemHtml
});

const ContactListView = Mn.CollectionView.extend({
  tagName: 'ul',
  className: 'list-group',
  childView: ContactItemView
});

DataBinding is a Behavior class that automatically binds the html using rivets.

We define ContactListView that will render the markup of a Bootstrap list group using ContactItemView which is responsible to render each list item.

Now the main view, still in contacts/view.js, responsible for defining the layout

export default Mn.View.extend({
  html: require('./template.html'),
  behaviors: [DataBinding],
  regions: {
    contactlist: '.contact-list',
    outlet: '.contact-outlet'
  },
  initialize(options) {
    this.contacts = options.contacts
  },
  onRender() {
    this.showChildView('contactlist', new ContactListView({collection: this.contacts}))
  }
})

It defines two regions: contactlist and outlet. The onRender event is used to show a ContactListView instance  in the contactlist region.

Now tell the contacts route (contacts/route.js) what view to render:

[..]
import ContactsView from './view';

export default Route.extend({
  [..]
  viewClass: ContactsView,
  viewOptions() {
    return {
      contacts: this.contacts
    }
  }
})

Do a new build a launch index.html#contacts

 What? A blank page? An "Error: No outlet region" in console? There's a missing piece in route configuration:

//main.js
import Mn from 'backbone.marionette';

[..]

router.rootRegion = new Mn.Region({el: '#app'});

We need to define a root region in router where the top level routes will show its views.

If everything went right, reloading should show a unstyled list:





Time to configure Bootstrap. Copy bootstrap.min.css and glyphicons-halflings-regular.eot to assets folder and update index.html:

<head>
    [..]
    <link rel="stylesheet" href="assets/bootstrap.min.css">
    <link rel="stylesheet" href="src/styles.css">
</head>

Much better.

Is possible to load Bootstrap using webpack, and the initial idea was to use in this tutorial, but, it requires additional configuration so, for sake of simplicity, just using the static files directly.

Setting the default route


Until now, is necessary to update the URL hash manually to go to contacts route. To automatically go to a default route there are a couple of options:

Update the location hash before start listening (it will work only at startup):

In main.js:


if (location.hash.length <= 1) {
  location.hash = '#contacts'
}
router.listen();

In application route activate, a not so good approach since ties the ApplicationRoute class to "/" path:

activate(transition){
    this.api = this.api || new WebAPI();
    if (transition.path === '/') {
      transition.then(function () {
        transition.redirectTo('contacts');
      });
    }
  }

And finally using "before:transition" event, which we will use given its flexibility:

Radio.channel('router').on('before:transition', function (transition) {
  if (transition.path === '/') {
    transition.redirectTo('contacts')
  }
});

router.listen();


Using redirect both in activate and in 'before:transition' event requires removing the abstract flag from application route options.

The contact detail


The view code (contactdetail/view.js) is pretty slim. Aside from defining the html and configuring the data binding, it maps an HTML event ('click #save-contact') to a view event ('save:model') using the triggers property:


import Mn from 'backbone.marionette';
import DataBinding from '../databinding';

export default Mn.View.extend({
  html: require('template.html'),

  behaviors: [DataBinding],

  triggers: {
    'click #save-contact': 'save:model'
  }
});

The markup is a basic Bootstrap form annotated with some rivets directives.

Let's implement the Route class that will be responsible to inject the model and listen to the view events.


In contactdetail/route.js:


//ContactDetailRoute
export default Route.extend({
  activate(transition){
    let contacts = this.getContext(transition).request('contacts');
    this.contact = contacts.findWhere({id: +transition.params.contactid});
    if (!this.contact) {
      throw new Error('Unable to resolve contact with id', transition.params.contactid);
    }
  },

  viewClass: ContactDetailView,

  viewOptions() {
    return {
      model: this.contact.clone()
    }
  },

  viewEvents: {
    'save:model': 'onSaveModel'
  },

  onSaveModel(view) {
    let attributes = _.clone(view.model.attributes);
    this.contact.clear({silent:true}).set(attributes);
  }

})


Is necessary to update contacts/route.js with the contextRequests definition

//ContactsRoute
export default Route.extend({
  [..]
  contextRequests: {
    contacts: function () {
      return this.contacts
    }
  }
})

In activate, we get a reference for the contacts collection and then the contact model corresponding to the contactid passed through transition.params. Noteworthy the usage of route context to retrieve the contacts collection, instead of using a global Radio channel, like was done for the WebAPI instance.

The request will be resolved by the nearest parent (ContactsRoute) that registered a reply using contextRequests property. This features allows to use the same Radio semantics in a scoped way thus helping to prevent the global Radio namespace be cluttered.

In viewOptions, a clone of the model is injected into the view and in onSaveModel (bound to 'save:model' event through viewEvents) the changes are saved back to the original model instance. This approach seems overkill in this example, since we could simple pass the actual model instance and let the view save it directly, but it allows to make reusable views. For example, we can use ContactDetailView to also edit a newly created contact.

Rebuild and go to #contacts/1. You should be able to view, edit and save the contact info.

A note about nested rendering

One of the  Marionette Routing benefits is the ability to handle nested state in a consistent manner. It also allows to nest views with a minimal declarative configuration. When a route is configured to show a view, it will look for the nearest parent route with a rendered view which must have a region named "outlet". If no parent route with a rendered view is found, the rootRegion is used.

In our app, the ContactDetailView is rendered into the outlet region defined in ContactsView. Note also that when changing from, e.g, #contacts/1 to #contacts/2 only contact detail view is rerendered.

Refinements


To select a contact when click in a contact list item is as simple as setting a click event in ContactItemView and do a transitionTo request in router channel:


const ContactItemView = Mn.View.extend({
  [..]
  events: {
    'click': 'onClick'
  },
  onClick(e) {
    e.preventDefault();
    Radio.channel('router').request('transitionTo', 'contactdetail', {contactid: this.model.get('id')})
  }
});

We could also render the a element with the appropriate href attribute value that would work the same.

Highlighting the selected contact item, requires a bit more of work. In ContactsView.setSelected we get the ContactListView instance using getRegion, resets any item with an 'active' class, find the ContactItemView instance associated with the passed contactid using children.find and add 'active' class to its element.

Since the view is only rendered after all routes are activated, setSelected is called inside a resolve handler of the transition. Note that the "before:activate" event is registered in activate and unregistered in deactivate to catch only events of child routes.

//ContactsView
export default Mn.View.extend({
   [..] 
   setSelected(contactId) {
    let listView = this.getRegion('contactlist').currentView
    listView.$('.list-group-item').removeClass('active')
    if (contactId) {
      let itemView = listView.children.find(function (view) {
        return view.model.get('id') == contactId
      })
      if (itemView) {
        itemView.$el.addClass('active')
      }
    }
  }
})

//ContactsRoute
export default Route.extend({
  activate(){
    this.listenTo(Radio.channel('router'), 'before:activate', this.onChildActivate);
    [..]
  },

  [..]

  onChildActivate(transition, route) {
    if (!transition.params.contactid) {
      return;
    }
    //must be done after transition finishes to ensure view is rendered
    transition.then(() => {
      this.view.setSelected(transition.params.contactid)
    })
  },

  deactivate() {
    this.stopListening(Radio.channel('router'), 'before:activate')
  }
})

    
This approach demonstrates how to catch events from child routes but is a lot of code for a common use case. With this in mind i created a Marionette Behavior (RouterLink) that takes care both of active class update as well click event configuration. This commit show how to use it.

Finally, let's render a view in the contact detail placeholder when no contact is selected. This can be done through a default child route for contacts route:


//main.js:

router.map(function (route) {
  route('application', {path: '/', routeClass: ApplicationRoute}, function () {
    route('contacts', {routeClass: ContactsRoute, abstract: true}, function () {
      route('contacts.default', {path: '', viewClass: ContactNoSelectionView,
        viewOptions: {message: 'Please Select a Contact.'}})
      route('contactdetail', {path: ':contactid', routeClass: ContactDetailRoute})
    })
  })
});

Radio.channel('router').on('before:transition', function (transition) {
  if (transition.path === '/') {
    transition.redirectTo('contacts.default')
  }
});

ContactNoSelectionView is an unremarkable Marionette view.

Important here is the definition of 'contacts.default' route with an empty path. It means that it will be activated when URL matches the path of the parent route, i.e., it becomes the default child route.

Another point is the absence of a routeClass option. Instead, viewClass and viewOptions are defined directly, useful for views that do not require customized logic to be instantiated.

Final thoughts


In days where most web development is done with React and Angular, using Backbone and Marionette seems outmoded. But i'm confident they still has a place.

After struggling with various approaches, i think the architecture presented in this tutorial, is suitable for large scale applications. More than an organized way of loading, manipulating state, it allows to create reusable / independent modules.

This application is an example. We could transplant all the code contained in contacts route and below, including HTML, as a sub application of a bigger one.  The only requirement is to have a Radio 'api' channel with a suitable 'getContactList' request, no need to worry what URL will be used or where it will render. In this way, we could develop sub applications independently assembling them into the full application just swapping the route configuration.

There are still some improvements to be done, like the possibility of add a contact, show a loading indicator, implement transition effects and prevent the exit from the contact detail route when there are pending changes. It will be left for an eventual second part of this tutorial.

sábado, 29 de outubro de 2016

Bringing order to a Marionette app: routing

As pointed in a previous post, i was using the recommended strategy of using the router as an entry point and handling state change through application interfaces instead of letting the URL changes drive the state transition.

While it worked, i soon realized that was not a DRY or scalable approach. So, after searching - and not finding - for libraries that provides the desired functionality (nested routes, async friendly), i decided to bite the bullet and implement an alternative.

Instead of relying on Backbone.Router or writing one from scratch, i used cherrytree as underlying engine, keeping to me the work to map the route transitions to Marionette concepts. What is accomplished so far:

Load data asynchronous


This is one of the key functionalities of routing libraries. In my previous approach, a pages object was defined with each entry defining the path and view options, the later being resolved asynchronous. Like below:

  pages: {
    newbuy: {
      path: 'newbuy',
      options: function(symbolName) {
        if (symbolName) {
          return dataService.getOwnedSymbols().then(function (data) {
             return {symbol: data.findWhere({name: symbolName})};
          });
        } else {
          return {symbol: null}
        }
      }
    }
  }

When no view class is explicitly defined it fallbacks to require the page name. dataService.getOwnedSymbols does an ajax request, caches the response and returns a promise

With the new routing strategy, each route is defined in a class and an activate hook is resolved asynchronous. The view options is resolved in an separated method:

//new buy
export default Route.extend({

  viewClass: ShareBuyView,

  viewOptions() {
    return {symbol: this.symbol}
  },

  activate(transition){
    let symbolName   
    if (symbolName = transition.params.symbolname) {
      return dataService.getOwnedSymbols().then((data) => {
        this.symbol = data.findWhere({name: symbolName});
      });
    }
  }
})

Its nicer, more organized but with same functionality, nothing that most Backbone routers could not handle

Nested routes / states


Since the app can be started with any URL and only one route is activated at time, in all routes i had to call dataService.getOwnedSymbols to ensure the availability data.

Here, the nested feature shines. When a nested route is activated we are sure that all parent routes where successfully activated, allowing to write something like:

//parent route
export default Route.extend({

  initialize() {
    Radio.channel('data').reply('ownedsymbols', this.getOwnedSymbols, this)
  },

  getOwnedSymbols() {
    return this.ownedsymbols  },

  activate(){
    return dataService.getOwnedSymbols().then((symbols) => {
      this.ownedsymbols = symbols
    })
  }
})

//child route
export default Route.extend({

  viewOptions() {
    return {
      collection: this.ownedsymbols    }
  },

  activate(){
    this.ownedsymbols = Radio.channel('data').request('ownedsymbols')
  }
})

Related commit

Note that in child route activate, the data is retrieved with a request to the Radio handler configured in the parent route. Since this radio call is synchronous we could even call it inside viewOptions making the activate hook optional here.

Route context


Setting global Radio channels and calling them through application is fine but can become hard to maintain as the project size and number of channels / registered requests grows.  An alternative is to pass down the required data into the application hierarchy, another approach that starts to be annoying as the hierarchy level count increases.

In the application i'm doing Marionette experiments, in some routes is necessary to retrieve a symbol instance. Since there are multiple symbols, this data is not suited to be stored / retrieved with a Radio global channel, so the current approach is to first load the global data than use it to find the symbol instance like below:

export default Route.extend({
  activate(transition){
    let buy   

    let ownedsymbols = Radio.channel('data').request('ownedsymbols')
    let symbol = ownedsymbols.findWhere({id: transition.params.symbolid});
    if (!symbol) {
      throw 'Unable to find symbol ' + transition.params.symbolid    }
    buy = symbol.buys.findWhere({id: transition.params.sharebuyid});
    if (!buy) {
      throw 'buy not found ' + transition.params.sharebuyid    }
    this.buy = buy    this.symbol = symbol  }
})
That's where the concept of route context comes in handy as a way to provide scoped data with easiness of Radio:

//a parent abstract route, activated when a symbol is loaded:
export default Route.extend({

  contextRequests: {
    'symbol': 'getSymbol'  },

  getSymbol() {
    return this.symbol  },

  activate(transition){
    let ownedsymbols = Radio.channel('data').request('ownedsymbols')
    this.symbol = ownedsymbols.findWhere({id: transition.params.symbolid})
    this.symbol.loadBuys()
  }
})
//a child route:
export default Route.extend({
  activate(transition){
    let buy    
    let symbol = this.getContext(transition).request('symbol');
    buy = symbol.buys.findWhere({id: transition.params.sharebuyid});
    if (!buy) {
      throw 'buy not found ' + transition.params.sharebuyid    }
    this.buy = buy    
    this.symbol = symbol  
  }
})

Related commit

Reuse route classes


It's possible to use same route class in different routes with the behavior defined by its context. Below is a route class that loads a symbol instance depending of params state (it can be called with or without symbolname param):
export default Route.extend({

  activate(transition){
    let symbolName    let ownedsymbols = Radio.channel('data').request('ownedsymbols')
    if (symbolName = transition.params.symbolname) {
      this.symbol = ownedsymbols.findWhere({name: symbolName});
    }
  }
})
//routes mapping:
router.map(function (route) {
  route('application', {path: '/', routeClass: ApplicationRoute, abstract: true}, function () {
    route('newbuy', {path: 'newbuy/:symbolname?', routeClass: NewBuyRoute})
    route('symbol', {path: 'symbols/:symbolid', routeClass: SymbolRoute,  abstract: true}, function () {
    })
    route('ownedsymbols', {path:':path*', routeClass: OwnedSymbolsRoute})
  })
});

Rewriting to use context:

export default Route.extend({
  activate(transition){
    this.symbol = this.getContext(transition).request('symbol');
  }
})

//The new routes mapping:

router.map(function (route) {
  route('application', {path: '/', routeClass: ApplicationRoute, abstract: true}, function () {
    route('newbuy', {path: 'newbuy', routeClass: NewBuyRoute})
    route('symbol', {path: 'symbols/:symbolid', routeClass: SymbolRoute,  abstract: true}, function () {
      route('symbolnewbuy', {path: 'newbuy', routeClass: NewBuyRoute})
    })
    route('ownedsymbols', {path:':path*', routeClass: OwnedSymbolsRoute})
  })
});

Related commit

Note a new 'symbolnewbuy' route, child of 'symbol' route, with the same class as 'newbuy' route. Retrieving symbol instance is cheap here, but is easy to foresee situations where the ability to reuse data already loaded in parent routes can improve performance.

This routing strategy opens the door to other improvements, not described here, like offloading some of the work that is currently done in the view.

Final remarks


Given the small amount of time invested in this new routing strategy, a week, the results so far are satisfactory. This new implementation allowed to replace the old router with large advantages, providing an intuitive way to do routing and primitives to better structure a Marionette application

On the other hand, is not ready for production. Its somewhere a proof of concept and alpha quality code. Some tasks in the queue to get in a good shape:
  • Tests
  • Examples
  • Adjust the interface, e.g, instead of returning a promise in activate to be resolved, pass a second argument to resolve explicitly (more control to developer)
  • Implement viewEvents
  • Define the behavior when the params of an already activated route changes (reactivate ? an event? an new hook?) 
  • Create a Marionette.Behavior to generate links according to markup
  • See if bundle splitting can be accomplished
  • See if is possible to define sub routes in route class definition
There are some drawbacks:
  • Duplication of code (Backbone.Router / Marionette.Router <> cherrytree)
  • Requires a Promise implementation

quinta-feira, 11 de agosto de 2016

Bringing order to a Marionette app: data binding

One of the problems i stumbled in my first working Marionette application was data binding / templating. I used a mix of Handlebars for read only views and Stickit for forms.

This solution showed its shortcomings soon, as described in an early post. So i decided to change the strategy and use a markup based binding solution, which is becoming quite popular (Aurelia, Vue etc). In the Backbone world, the most popular libraries are Rivets and Epoxy, the former being the chosen one since fits my needs better.

Here are the remarks of the migration with the advantages and problems found

Meeting Share Monitor

This is an app to monitor the stock prices of Bovespa (Brazilian stock exchange). It stores the owned shares with buy price and compare with current prices to see how much the investor won (or loose). While simple and only targeted for my personal usage, is fully functional.

Technically is a web mobile app, that uses Ionic CSS to style. The share buy info is stored in local storage. There’s also a backend written in Pascal that retrieves the stock info from Yahoo finance (needed to avoid same origin issue and normalize the data format). Sources here. Online version here.

Problem 1: dynamic DOM update

The app fetches stock info regularly from the backend and updates the owned stocks prices and state. Here’s the code to update each list view item using Handlebars and jQuery:

//template
  <div class="col">{{name}}</div>
  <div class="col stock-price">
    {{formatCurrency latestPrice}}
  </div>
  <div class="col stock-variation {{ifThen (isNegative latestVariation) 'negative-value'}}">
    <i class="ion-arrow-{{ifThen (isNegative latestVariation) 'down' 'up'}}-a"></i> {{suffix (formatFloat (absolute latestVariation)) '%'}}
  </div>
  <i class="icon ion-chevron-right icon-accessory"></i> 
  //view
  onLatestPriceChange: function () {
    var price = this.model.get('latestPrice');
    var variation = this.model.get('latestVariation');
    var variationText = variation ? Math.abs(variation).toFixed(2) + '%' : '--';
    var iconClass = 'ion-arrow-' + (variation > 0 ? 'up': 'down') + '-a';
    this.$el.find('.stock-variation').toggleClass('negative-value', variation < 0)
      .contents().filter(function() {
      return this.nodeType == 3 && this.textContent.trim().length > 0
    }).replaceWith(' ' + variationText);

    this.$el.find('.stock-variation i').removeClass('ion-arrow-up-a ion-arrow-down-a').addClass(iconClass);
    this.$el.find('.stock-price').html(price ? 'R/pre> + price.toFixed(2) : '--');
  }

I could just call render and replace the content altogether but seems a bit overkill just to update the value of a few nodes values and classes.

Problem 2: collection view boilerplate

I used a CompositeView to hold the list of owned stocks (can’t remember exactly why not used a CollectionView). This is the template:

<ul id="symbol-list" class="list">
</ul>

That’s right. A separated template file with two lines of pretty generic code. To assemble the list, needs two view classes each own with a bunch of configuration:

var OwnedSymbolItemView = Marionette.ItemView.extend({
 template: require('../templates/ownedsymbolitem.hbs'),
 tagName: 'li',
 className: 'item item-icon-right row',
  modelEvents: {
    'change:latestPrice': 'onLatestPriceChange'
  },
  events: {
    'click': 'onClick'
  },
  onLatestPriceChange: function () {
    [..]
  },
  onClick: function (e) {
    e.preventDefault();
    Radio.channel('navigation').request('goToPage', 'symboldetails', this.model.get('id'));
  }
});

var OwnedSymbolsView = Marionette.CompositeView.extend({
  template: require('../templates/ownedsymbols.hbs'),
  childView: OwnedSymbolItemView,
  childViewContainer: '#symbol-list',
  pageHeader: {..},
  onAddBuy: function () {
      Radio.channel('navigation').request('goToPage', 'newbuy');
  }
});

Now the code after migration to rivets:

var OwnedSymbolsView = Marionette.ItemView.extend({
  template: false,
  html: require('../templates/ownedsymbols.html'),
  pageHeader: {..},
  onBeforeRender: function () {
    if (this.view) {
      this.view.unbind();
      this.view = null;
    }
    this.attachElContent(this.html);
    this.view = rivets.bind(this.el, this);
  },
  onDestroy: function () {
    if (this.view) {
      this.view.unbind();
      this.view = null;
    }
  },
  onAddBuy: function () {
      Radio.channel('navigation').request('goToPage', 'newbuy');
  },
  onItemClick: function (e, scope) {
    Radio.channel('navigation').request('goToPage', 'symboldetails', scope.model.get('id'));
  }
});

A single view class with just the necessary code to initialize rivets binding, which can easily extracted into a behavior.

Template diff. View diff.

Problem 3: form boilerplate

I used Stickit to bind forms. With rivets i got rid of code like:

bindings: {
    '[name=symbol]': {
      observe: 'symbol',
      events: ['blur']
    },
    '[name=price]': {
      observe: 'price',
      onGet: 'numToStr',
      onSet: 'strToNum'
    },
    '[name=quantity]': {
      observe: 'quantity',
      onGet: 'numToStr',
      onSet: 'strToNum'
    },
    '[name=date]': 'date'
  },
  strToNum: function (val) {
      if (val != null) {
        return +val;
      }
    },
    numToStr: function (val) {
      if (val != null) {
        return val.toString();
      }
    }

Also could simplify the binding of view related computed values. With Stickit i needed to create a separated Backbone Model to hold the view state and bind it separately from actual model. Now i can use a simple object to hold the state, so no more:

this.stickit();
    this.stickit(this.state, {
      '#remove-buy': {
        observe: 'isNew',
        onGet: function (value) {
          return !value
        },
        visible: true
      },
      '[name=symbol]':{
        attributes: [
           {
          name: 'disabled',
          observe: 'hasSymbol',
         onGet: function (value) {
           return value
         }
        }]
      }
    })

Template diff. View diff.

Final remarks

  • The view code simplification accomplished with the migration to Rivets by far outweighs the extra markup required
  • Rivets is not as light weight as it seems. Every single binding instance holds the global library options and attaches bound functions at will. The iteration each binder copies scope properties to every child view regardless of nest level
  • I had to add some functionality in rivets, like ability to change the event used by input
  • The Backbone adapter recommends to bind to Collection.models with the default adapter but this will intercept direct changes to models array defeating the purpose of the adapter

segunda-feira, 28 de dezembro de 2015

Pain points of development with MarionetteJS

After sometime developing a fully functional application with MarionetteJS, i already have a good understanding of its inner working and what is good and what leaves to be desired.

The good is the small leaning curve, easily integrating with other web resources (Bootstrap, jQuery plugins etc). But using MarionetteJS is not hassle free.

Dynamic update of DOM

I use Handlebars as the template engine and i’m quite comfortably with it. Together with a Webpack loader, loading and using the template is straight and simple as could be. The problem is, after the first render, when the model data changes. I tried some options (with actual examples from the project i’m working):

Configure an event in the view to catch when data change and update using jQuery:

modelEvents: {
    'change:latestPrice': 'onLatestPriceChange'
  },
  onLatestPriceChange: function () {
    var price = this.model.get('latestPrice');
    var variation = this.model.get('latestVariation');
    this.$el.find('.stock-price').html(price ? 'R/pre> + price.toFixed(2) : '--');
    this.$el.find('.stock-variation').toggleClass('negative-value', variation < 0).html(variation ? (variation * 100).toFixed(2) + '%' : '--');
  }

This solution is far from perfect: duplicates the formatting done in template, becomes tedious, error prone and time consuming as the number of elements to update grows.

Re-render the whole view

This is the easiest, keeping the view logic in same place and the setup is minimal, but it comes with it’s own issues, with potential effects in performance and smoothness. Things are worse in LayoutView, where all regions must be also re-rendered.

Use a databinding library like Stickit

This solves the duplication of view logic and optimally updates the DOM. In the other side needs some configuration that, in my experience, is quite tedious to do. In one of my views, with the template below, i concluded that was a good call to use Stickit. But given the extra work to convert from Handlebars, i gave up and ended just rendering everything (including the regions)

<div class="leading-stock-price js-stock-price">{{formatCurrency latestPrice}}
    <span class="stock-variation js-stock-variation {{#if (isNegative latestVariation)}}negative-value{{/if}}">{{formatFloat latestVariation}}%</span>
</div>

<div class="leading-owned-stock">
    {{stockQuantity}} ações = {{formatCurrency stockValue}}
    <span class="stock-variation {{#if (isNegative valueVariation)}}negative-value{{/if}}">{{percentage valueVariation}} ({{formatCurrency valueDifference}})</span>
</div>

<div class="list card">
    <div class="item item-divider item-icon-right">
        Operações de compra
        <i class="icon ion-plus"></i>
    </div>
    <div class="js-buys">

    </div>

    <div class="item item-divider row">
        <div class="col-25 col-offset-50">{{formatCurrency meanPrice}}</div>
        <div class="col-25">{{formatCurrency buyValue}}</div>
    </div>
</div>

Boilerplate for simple lists

The need to define a CollectionView subclass, and ChildView is overkill for simple lists or tables. Below is the views and template for a really simple list:

var BuyItemView = Marionette.ItemView.extend({
  template: require('../templates/buyitem.hbs'),
  className: 'item item-icon-right',
  serializeData: function () {
    return _.clone(this.model.attributes)
  }
});

var BuysView = Marionette.CollectionView.extend({
  childView: BuyItemView
});

//template
<h3>{{dateToLocale date}}: {{quantity}} ações a {{formatCurrency price}}</h3>
<p>Total: {{formatCurrency totalPrice}}</p>
<i class="icon ion-chevron-right"></i>

Aside from the boilerplate, the templates directory can become crowded fast (something that can be mitigated with a better directory layout)

Switching code editor between view <-> template <-> css

For more that you try to make the view independent of the template, often is needed to change both to accomplish some UI changes. The fact that the attributes of root element are defined in the view class does not helps. Definitively hurts productivity.

Where to go from here?

I’m a desktop developer with some very large projects (hundreds of forms/views) over my shoulders. Currently i’m in the phase of experimentation, using a hobby project as field, with no time pressure. I plan to add a web client to the main projects, eventually replacing the desktop client.

It’s clear that, as i’m doing today, is not doable to do the serious work with JS/HTML/MarionetteJS. Just feel / is little productive. But it does not mean i gave up. There’s some ideas i have in mind to boost my experience:

Use another databinding library like RivetsJS

When i first looked at RivetsJS, i think: no, never, its markup is weird, i want clean standard html. Also, there are some patterns, like rendering collection in a template loop or defining the callback in the template that is/was regarded as bad practice. With the rise of Angular, React, RiotJS and VueJS seems that those patterns are no more weird.

With such library, the dynamic DOM update and the boilerplate for simple lists could be tackled. At first look, does not suffers from the time consuming configuration step required by StickIt. In fact, it would mostly replace the Handlebars templating. The down side is the lack of project updates, since the main author pointed little interest to continue its development.

See what smart DOM updates libraries can do

This would improve the DOM update since re-render would be much less cost, but initiatives like handlebars-idom are in its infancy.

Search/develop ways to handle related code together

It’s pretty interesting the ability to have JS, template and CSS in the same file as Vue does. It uses a Webpack loader (or Browserify plugin) to precompile into regular code. Searched for a generic loader in the same line without success. An alternative is to see if is doable to adapt vue-loader.
Another try is to look for a WebStorm extension that does something similar at IDE level.</div>

domingo, 18 de outubro de 2015

Routing in Backbone: to trigger or not to trigger

One of most prominent advices regarding routing in Backbone is to not use the Router to switch states after the application is loaded. As consequence, the usage of hyperlinks or {trigger: true} option in Router.navigate are discouraged. This recommendation is in both Addy Osmani and David Sulc books. For reference, David Sulc's routing chapter is available as a free sample.

Given such strong statements, it would be expected that this practice should be one of corner stones for well designed Backbone applications. But what to say when two of better structured Backbone examples available use the other way around? Both Marionette Wires and Gistbook uses the Router to handle state. In a broader scope, the major web frameworks / libraries use the URL to manage application state. It's something to reason about.

In the Marionette project i'm working, i follow the general advice and uses the router only as an entry point. See below for the general architecture.

As a side note, IMHO the Marionette.AppRouter adds very little to the default Router to justify its existence.


//a service to handle navigation (using pub/sub)

var Radio = require('backbone.radio');

var PageManager = Marionette.Object.extend({
 initialize: function () {
  this.navigationChannel = Radio.channel('navigation');
  this.navigationChannel.reply('showPage', this.showPage, this);
 },
        //page configuration how to load a view
 pages: {
  newbuy: {
   path: 'newbuy'
  },
  ownedstock: {
   path: function(id) {
     return 'ownedstock/' + id
   },
   options: function(id) {

   }
  }
 },
 showPage: function(name) {
  //..
 }
});


//router.js
var NavChannel = require('backbone.radio').channel('navigation');

var Router = Backbone.Router.extend({
 routes: {
  'newbuy': 'showNewBuy',
  'ownedstock/:id': 'showOwnedStock'
 },
 showNewBuy: function() {
  NavChannel.request('showPage', 'newbuy');
 },
 showOwnedStock: function (id) {
  NavChannel.request('showPage', 'ownedstock', id);
 }
});

//navigation inside app

  onAddBuyClick: function (e) {
    e.preventDefault();
    Radio.channel('navigation').request('showPage', 'newbuy');
  }

//the application is responsible for show the view in the proper region
var Application = Marionette.Application.extend({
  initialize: function () {
    this.router = new Router();
    this.rootView = new RootView({});
    this.pages = new PageManager();
    this.listenTo(this.pages, 'show:page', this.onShowPage);
  },
  onShowPage: function (view) {
    this.rootView.showChildView('content', view);
  },
  onStart: function() {
    console.log('app start');
    Backbone.history.start();
  }
});



sábado, 10 de outubro de 2015

In the way to a scalable application with Marionette

One of lessons learned while building my first javascript mobile application was the need to use a framework on top of Backbone. Marionette is the natural choice given it’s popularity and active development.

I bought, and read, David Sulc’s introductory book for Marionette. It’s a great resource to get in touch with the framework but one of the topics that falls short is the structure, based in modules, a deprecated feature and discarded even by it’s creator. There’s a follow up book that explains how to use RequireJS with it.

After writing a working example i felt the next step was build a full application, even if small. It should use a sane architecture to serve as base for future larger applications.

The search for reference applications

I remembered that the Marionette homepage has links to some example applications so i looked into them for possible starting points. Edit was discarded because of CoffeScript, Streamus seems tied to Chrome extension. Both Gistbook and Marionette Wires impressed me by its modular, organized architecture. In the down side they use specialized components (router) and Babel/ES6 and i don’t want, at my current stage, to learn so many new technologies/tools at same time.

As a last resort, i evaluated TodoMVC. A quick look at the code showed that is not what i was looking for. It uses global variables (although well organized into a namespace) and lacks a few common iterations like page transitions.

Kicking off

Since none of the example applications could be used as starting point, i setup the project from scratch. After the usual installation of packages and configuration of module loader (Webpack), i got stuck at the application bootstrap/start.

In the Application section of the Marionette documentation, the Application Regions feature is marked as deprecated and suggest to use a root LayoutView, something like:

var RootView = Marionette.LayoutView.extend({
  el: 'body'
});

//followed by

app.rootView = new RootView();

I did something similar…

var RootView = Marionette.LayoutView.extend({
  el: 'body',
  regions: {
    'header': '#app-header',
    'content': '#app-content'
  }
});

var Application = Marionette.Application.extend({
  initialize: function () {
    this.rootView = new RootView({});
  },
  onStart: function() {
    console.log('app start');
  }

});

//followed by

var app = new Application();
app.start();
app.rootView.showChildView('content',new MyView());

… and got an error message:

Error: An “el” #app-content must exist in DOM

After a while i figured that i have to put the script at the end of html file or wrap the initialization code in $.ready handler. It took some time to show the first view, but is rewarding anyway.

Decouple roads

To write a scalable application is necessary a mechanism to navigate through it. While a basic requirement is also a hard one to implement in a decoupled, flexible and uniform way. Below a some patterns i found in the above examples:

  • Execute a method or trigger an event in the app instance. The example of A Gentle Introduction book does this. But in a not uniform way.
    Sometimes triggers an event
contactsListView.on("childview:contact:show", function(childView, args){
   ContactManager.trigger("contact:show", args.model.get("id"));
});

…sometimes updates the application regions directly.

contactsListView.on("childview:contact:edit", function(childView, args){
   var model = args.model;
   var view = new ContactManager.ContactsApp.Edit.Contact({
     model: model
   });
   //...

  ContactManager.regions.dialog.show(view);
});

This pattern has the drawback of tying to the application instance and interface.

  • Use hyperlinks and let the router do the actual work. This approach is used in Marionette Wires example:
  //a  book route is defined
  routes: {
    'books/:id' : 'show'
  },

  show() {
    return new ShowRoute({
      layout: this.layout
    });
  }
  
  //in the view a hyperlink is created for each book 
  export default ItemView.extend({
    template: template,
    tagName: 'a',
    attributes() {
      return {
        href  : `#books/${this.model.get('id')}`
      };
    }
  });

This pattern has some criticism that may not be the case in the specific app since it uses an customized route to take care of state. Anyway it requires a pretty opinionated architecture which is no-go for me at least for now.

  • Uses a pub/sub mechanism. Gistbook uses Radio to request a view to be show. Pretty simple and straightforward:
  //configure the handler 
  var RootView = LayoutView.extend({
  initialize() {
    var containerRegion = this.getRegion('container');
    Radio.comply('rootView', 'showIn:container', containerRegion.show, containerRegion);
  }
});
 //later do the request
  export default Route.extend({
   show() {
    var gistView = new GistView({
      model: new Gist(),
      ownGistbook: false,
      homePage: true
    });
    Radio.command('rootView', 'showIn:container', gistView);
 }
});

Gistbook also uses an specialized router to take care of state. In fact i could not get the full picture of how it works. Anyway, using pub/sub pattern seems a good idea even if used without a state router.

With base structure done, is time to some coding. Let’s see results.

domingo, 20 de setembro de 2015

Using modules outside of app root directory with WebPack

In order to evaluate different mobile UI  frameworks (Ionic, jQuery Mobile etc) i started a project with multiple client apps, each one completely independent with its own configuration. The business logic layer, a couple of Backbone models, is the only code shared by the apps.

It has the following directory structure, where the models resides in the shared folder. At the same level, there's ionic-mn folder which holds one of the mobile clients including package.json, webpack.config.js etc.


  |-shared
  |---models
  |-ionic-mn
  |---css
  |---fonts
  |---node_modules
  |-----backbone
  |---scripts

The problem started when i tried to resolve the js files in the shared folder. Reading the WebPack site i managed to do it modifying resolve.root, so i ended with the following config:

var path = require('path');

module.exports = {
    entry: "./scripts/main.js",
    output: {
        path: __dirname,
        filename: "bundle.js"
    },
    resolve: {
       root: [path.resolve('.'), path.resolve('../shared')]            
    }
};

With this i succeeded resolving the shared entity.

var OwnedSymbol = require('models/ownedsymbol');

But started  to get a different error:

ERROR in ../shared/models/ownedsymbol.js
Module not found: Error: Cannot resolve module 'backbone' in D:\repositories\sharemonitor\clients\shared\models

This occurs because ownedsymbol.js tries to resolve 'backbone' module. I looked for info in net without luck. Tried to use ProvidePlugin and also did not work. In the end i did the hard way:
    ..
    //in webpack config
    module: {
        loaders: [
          { test: require.resolve("backbone"), loader: "expose?Backbone" },
          { test: require.resolve("underscore"), loader: "expose?_" }
        ]
    ..
Yes, i took the heretic path and set Backbone and underscore to a global variable. It works but diverges from the idiomatic module usage so i tried with the imports-loader:
    ..
    //in code:
    var OwnedSymbol = require('imports?backbone=Backbone,underscore=_!models/ownedsymbol'); 
    ..
This solution would avoid the global variable issue, the only problem is that does not work. I get:

ERROR in ./~/imports-loader?backbone=Backbone,underscore=_!../shared/models/ownedsymbol.js
Module not found: Error: Cannot resolve module 'Backbone' in D:\repositories\sharemonitor\clients\shared\models

While i don't discover a better solution, i'll stick to the expose/global path. If the price to start coding is being a herege...

Update


The WebPack developer sokra pointed at gitter to try resolve.fallback, so i did:

   
  resolve:
     {
       root: [path.resolve('.'), path.resolve('../shared')],      
       fallback: __dirname + '/node_modules'
     }

With this configuration in place the build worked but with duplication of some modules. I managed to resolve the duplication using npm dedupe plus WebPack dedupe plugin.
So far, so good, now i have the option to stay clean avoiding global variables, at the cost of some extra configuration.