Search code examples
backbone.jsmarionette

How can I show and hide regions in Marionette.js?


I am trying to figure out how to use a router and controller in my Marionette.js application. I am able to start the initial page in my App's start handler, but I can't seem to figure out how to handle other routes. This SPA isn't complex, where I only have three pages for my users. One is a leads table view, a vehicle table view, and a view of a single vehicle. I'm not worried about the single vehicle view until I figure out how this routing works.

// my app
var App = new Marionette.Application({});

// my lead and vehicle model rows
App.vehicleRowView = Marionette.ItemView.extend({
  tagName: 'tr',
  template: '#vehicle-row-tpl'
});
App.leadRowView = Marionette.ItemView.extend({
  tagName: 'tr',
  template: '#lead-row-tpl'
});

// composite views for the tables
App.vehicleTableView = Marionette.CompositeView.extend({
  tagName: 'div',
  className: 'row',
  template: '#vehicles-table',
  childViewContainer: 'tbody',
  childView: App.vehicleRowView
});
App.leadsTableView = Marionette.CompositeView.extend({
  tagName: 'div',
  className: 'row',
  template: '#leads-table',
  childViewContainer: 'tbody',
  childView: App.leadRowView
});

// controller
var Controller = Marionette.Object.extend({
  leads: function() {
    var leadstable = new App.leadsTableView({
      collection: this.leads
    });
    App.regions.leads.show(leadstable);
  },
  vehicles: function() {
    console.log('vehicles...');
  }
});

// router
var AppRouter = Marionette.AppRouter.extend({
  controller: new Controller,
  appRoutes: {
    'leads': 'leads',
    'vehicles': 'vehicles'
  }
});
App.router = new AppRouter;
App.vehicles = [];
App.leads = [];


// Start handlers
App.on('before:start', function() {
  this.vehicles = new Vehicles();
  this.vehicles.fetch();
  this.leads = new Leads();
  this.leads.fetch();

  var appContainerLayoutView = Marionette.LayoutView.extend({
    el: '#app-container',
    regions: {
      vehicles: '#vehicles-content',
      leads: '#leads-content'
    }
  });
  this.regions = new appContainerLayoutView();
});

App.on('start', function() {
  Backbone.history.start({pushState: true});
  var vehiclesLayoutView = new this.vehicleTableView({
    collection: this.vehicles
  });
  App.regions.vehicles.show(vehiclesLayoutView);
});

App.start();

On start, the front page is fine. However, when I go to #leads, my leads table doesn't render. Actually, the route doesn't happen, and the URL changes to /#leads. If I then go to that URL, the table skeleton renders, but not the data. The collections are loaded fine on before:start, and the templates are fine. I have to go to the URL twice, but the table has no data, even though my App.leads collection is loaded fine. My console.log output confirms I am hitting the route, though.

I want to hide the vehicles region when the user goes to the #leads route. When the user goes to #vehicles, I then want to hide my leads table and display the vehicles (same view from my start handler).

I feel like I'm right there, but missing something basic.


Solution

  • By looking at your vehicles and leads regions, I have a suspicion you've misunderstood the role of regions. If you expect them to swap one another, then you would create just one region and have that region .show( new VehiclesView() ); when you want to show vehicles, and .show( new LeadsView() ); when you want the leads to replace the vehicles.

    And here's a working example:

    var app = new Mn.Application();
    
    var Controller = Mn.Object.extend({
      leads: function(){
        app.regions.setActive('leads').getRegion('main').show( new LeadsView() );
      },
      vehicles: function(){
        app.regions.setActive('vehicles').getRegion('main').show( new VehiclesView() );
      }
    });
    
    var VehiclesView = Mn.ItemView.extend({
      template: _.template('،°. ˘Ô≈ôﺣ » » »')
    });
    var LeadsView = Mn.ItemView.extend({
      template: _.template("( /.__.)/ (.__.)")
    });
    
    var AppLayoutView = Mn.LayoutView.extend({
      el: '#app',
      regions: { main: 'main' },
      events: { 'click nav a': 'onClick' },
      onClick: function(evt){
        evt.preventDefault();
        var viewName = evt.currentTarget.dataset.view;
        app.controller[viewName]();
        app.router.navigate(viewName);
      },
      setActive: function(viewName){
        /** it might seem that it is easier to just 
        make the link bold on click, but you would have 
        to handle it if you want to make it active on page load */
        this.$('nav a').
          removeClass('active').
          filter('[data-view="'+viewName+'"]').
          addClass('active');
        return this;
      }
    });
    
    app.on('start',function(){
      
      app.regions = new AppLayoutView();
      
      app.controller = new Controller();
      app.router = new Mn.AppRouter({
        controller: app.controller
      });
      Backbone.history.start({pushState: true});
      
      /** show initial content */
      app.controller.leads();
      app.router.navigate('leads');
      
    });
    
    app.start();
    .active { font-weight :bold ;}
    <script src='http://code.jquery.com/jquery.js'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.2.3/backbone.js'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/backbone.marionette/2.4.3/backbone.marionette.js'></script>
      
    <div id="app">
      <nav>
        <a href="#" data-view="vehicles">Vehicles</a>
        <a href="#" data-view="leads">Leads</a>
      </nav>
      <main></main>
    </div>