Friday, August 16, 2013

A Walk-Through of the Backbone.Fiber Demo

As a follow up to my post about the Fiber framework, I wanted to step through how the demo was assembled to utilize various features offered in the framework.  My goal was to create a basic, single data source application that explored several different patterns you will encounter when building a SPA.  Even though its read-only, its still a good example of the capabilities and project organization.  While concepts like two-way binding and state management are all possible extensions, the framework itself is not really concerned with how data is bound and rendered but more with how different parts are loaded and work together to create a functional application.  Additionally, I'll touch on how the project is organized to leverage various developer tools.  You can find the source on GitHub and use the live demo here



Connecting to the Data

The demo uses data from the Rotten Tomatoes API to render a list of movies.  I prefer to define my collection and its model together in one file.  The result set needs to be parsed to flatten it out and reformat dates into something that can be rendered easily into a table.  I cherry-picked several fields from the list result set and purposefully added columns to the model that are not present in the list results such that they had to be loaded later when the detail was shown in a popover.

The only view that directly references the definition of the collection is the top-level shell view.  It adds the dependency to the module and is the only view that creates an instance.  All the other views in the application use a generic Backbone.Collection or Model object to hold subsets of the main source data set.  Those views simply expect data to exist in a certain format but do not care how that data is sourced.  Even the popover view, which calls the fetch() method on the model that represents the movie to show the detail for in the popover box, doesn't know its a movie model, it just expects the object to know how to get the data and define attributes it will consume in the rendered template.

The Layout

I saw this demo as an opportunity to try the first release candidate of Twitter Bootstrap 3.  I only found a few issues with features I used which appear to have been already fixed for a future release.  I used only a few minor styling overrides to the default classes either to customize the layout for my specific use or to work-around a minor issue in the framework. 

The layout begins with a fixed top navigation bar that contains links to browse the different lists available in the movie API.  A sidebar contains filters for the Critics Rating and the Release Date.  The default state includes the rows with the specified value.  The user can toggle each value to show/hide the matching rows from the list.  The main content areas holds the list of movies returned by the API and enables clicking on the title to show a popup with more detail.  One final UI element is the loading indicator which pops up each time you navigate to a different list.  This uses a combination of the built-in Bootstrap modal and progress bar components.  The loading box is probably where I made the most modifications to the CSS to achieve the desired look and effect I wanted when a new data set was being retrieved.

Project Organization

Another area I decided to explore while working on this demo was how to integrate various tools into my workflow.  I knew that while I was going to use RequireJS to load all my dependencies while developing, I'd probably want to consolidate those down slightly for a production version to help speed the load time.  Additionally, I've been experimenting with Bower to help ensure all the required dependencies are automatically installed and available to the application.  This makes it a lot easier for someone else to get the project up and running in their environment and can make it easier to keep all the sources up-to-date.  These goals impacted how I organized my project files such that I could develop and configure Grunt to generate a production build.

 |- app
 |  |- scripts
 |  |  |- models
 |  |  |  |- movies.js
 |  |  |- views
 |  |  |  |- shell.js
 |  |  |  |- shell.html
 |  |  |  |- ...
 |  |  |- main.js
 |  |- styles
 |  |- index.html
 |  |- config.js
 |- bower_components
 |- vendor_components


The app directory is really the root of the application. Since Bower installs dependencies in the project root under bower_components, I decided to add a vendor_components directory to store non-Bower aware dependencies. The config.js file maps these paths so RequireJS can find them. The scripts directory is the actual basePath so everything will load relative to this unless otherwise overridden in the path config. As part of my build process, I change the script tag that loads RequireJS so the config.js is not needed in the production release and all JS files are relative to the scripts directory. This implies that either those files need to be copied from the *_components directories or optimized into a bundled file. As a test, I moved moment.js such that it would not be included in the optimized bundle, but need to be loaded separately when the movies model loaded. The Gruntfile has a section that handles minifying these sources and moving them to the scripts/vendor directory in the final build.

The actual application will be defined in the models and views directory. I kept things pretty flat in the demo and didn't create any extra sub directories. Each view used will be defined individually in a JS file that will represent the name of the view referenced throughout the application. A template HTML file of the same name is required as well and will contain the markup to use in the rendering of the view. Once the project layout is defined, its time to create the actual views.

The Great Mediator

The shell view became the top-level view in the application and, as such, is where the source data is defined and the place where all the other views are coordinated. The shell view has several children - nav, sidebar, content, and popover.  Plus, it also directly manages the loading indicator element via the Bootstrap modal plugin.  As events occur in each child view, they bubble up to the shell view which reacts appropriately. 

This means when the user clicks a link in the nav view, it triggers a change event which the shell view handles by telling the main source collection to load a new list of movies from the API.  When the user toggles a filter in the sidebar, it bubbles up a change event to shell so it can apply the filter to the source collection and reset the contents collection.  If the title of a movie is clicked in the content view, that also bubbles up to the shell view so it can create the popover and tell it what movie model to load and where to align itself on the page.

While each child view manages its children, they all defer to the shell view to coordinate the other views on the page.  None of the children are aware nor do they care about any other views besides their children.  That's one of the primary goals of the Fiber framework - to encapsulate each view and provide a structured means to coordinate each other in a loosely-coupled manner.  Parents can access the instance of their direct children but not their children's children.  Children should communicate to their parent via events.  Deeper descendant's events could bubble past their immediate parent to a grandparent if the parent does not handle the event (or allows it to continue bubbling).

First Load, First Render

One of the challenges with designing a modular application is getting past the first load and rendering stage.  Loading is an issue because you have to ensure the module is actually loaded before trying to use it.  Rendering is an issue because the instance needs to be, minimally, created before you can reference it.  However, there may not be any data to render on the first pass but you may want the other content to be there so additional functionality can be loaded knowing that the data will get there eventually.  To handle some of these issues, Fiber has a couple helpers available to ensure views are loaded and manage the rendering life-cycle.

Step 1. Load Shell

When you load the application, index.html has nothing except the links to the style sheets, a script tag to load RequireJS, and a body tag with the data-view="shell" attribute.  Once RequireJS loads, it will load config.js which sets up the paths to point back to the Bower components directory and shim anything not AMD friendly.  After that is dealt with main.js is loaded which identifies everything to load before the actual application starts loading:

require( [
   'jquery',
   'backbone',
   'underscore',
   'backbone-fiber',
   'vendor/bootstrap',
   'vendor/jquery-ui',
   'vendor/jquery.outside-events',
   'text'
],
function( $, Backbone, _ ) {

      /* Configure components */

      _.templateSettings.variable = 'data';

      Backbone.Fiber.start();
});


Once those dependencies are ready, the application can be started by calling Backbone.Fiber.start(). This function simply looks for any element with a data-view attribute and starts trying to connect the defined views to the element. If the view module is not loaded, it will use RequireJS to fetch it along with its template and then create the instance and tell it to render using the target element as its container. Internally, render() asks Fiber to search for more data-view attributes and continue connecting instances to them.

Step 2. Load Shell's Children

The shell view template defines three data-view attributes - nav, sidebar, and content:

 


Because the render function asks Fiber to search for more views to load, nav, sidebar, and content will be loaded, created, and rendered as part of the shell views first rendering cycle. Since this is all happening asynchronously, there is no guarantee of the order those three views will load. Part of the nav view rendering is to ensure one of the menu items is selected and emit a change event. The shell view will be listening for that change event and will attempt to load data and reset the sidebar and content view's collections. Since those two views may not be created yet, its necessary to wait for them before proceeding. This is where the Fiber waitFor() function comes in handy. Call it with a list of child views you need to be ready before proceeding and it will attach to the promises for the modules and call the passed in callback once those views are ready:

   'change.nav': function( event, ui ) {

      var self = this;

      this.$progressBar.modal('show');

      this.waitFor( ['sidebar', 'content'], function() {
         self.source.load( ui.data.action ).done(function() {
            self.ready.call( self );
         });
      });
   },


Step 3. Keep Loading

Once shell fetches the data from the API, it will transform it as necessary and pass it down into the content and sidebar views. These two views will subsequently render their template. The content view has no other children views to load. The sidebar renders a filter-item view for each column that can be filtered. As each initial rendering runs, the views are loaded and created until there is no longer anything to load. After this initial rendering and loading, all the view modules are cached in memory and available for use. They don't need to be loaded any longer so that step can be skipped and the view can just be created. At this point everything has been loaded except the popover view. That content is not statically defined in the shell.html template file. Instead, its created dynamically as needed so until the user clicks a movie title, this view is not loaded.

Dynamic Connections

The other facet of Fiber is to dynamically generate DOM elements and connect a view to it. The most common use case for this functionality are for repeating elements or overlays. The demo did not utilize rendering each table row as a view bound to the model representing the row as there really wasn't a need for it in this situation. To demonstrate its use, I decided to dynamically load the popover that shows the details of a movie when you click a title in the table. The content view will emit a "select" event when the title is clicked. It will include a jQuery object representing the row and the model associated with the row. In the handler, the logic must check for the existence of the popover instance, and if not present, load and create it via connect():
   'select.content': function( event, ui ) {

      var popover = this.findChild( 'popover' );

      if ( popover ) {
         popover.show( this.source.get( ui.data.item ), ui.data.row );
      } else {
         this.connect( 'popover' ).done(function( popover ) {
            popover.show( this.source.get( ui.data.item ), ui.data.row );
         });
      }
   }
The connect() function assumes several things when you pass it a string. First, this string represents a view module that should be loaded. Second, the DOM element returned by factory() will be the container for the view. In this case, the default factory() implementation appends a DIV to the calling views container. For this demo, that works fine so there's no need to override the built-in behavior. The connect() function returns a promise which, when resolved, will pass the view instance representing the popover.

Those are the basics of the demo. You can see how Fiber helps alleviates some of the problems related to loading application modules and provides an opinion about how to build an application. The structure and helpers all work to enable simplifying managing the life-cycle on the presentation side, defining the relationships between components, and maintaining encapsulation of functionality.