Node v6
Starting with v0.19 Astro must be used with Node v6+!
Menu

Controllers

What you’ll learn and how you can apply it

By the end of this section, you’ll be able to:

And you'll be able to:

This course is for you because...

Pre-requisites

Why?

Code grows and becomes unapproachable and unmaintainable unless you separate it into manageable components. Astro apps implement components through the use of controllers. Controllers are well-known from the Model-View-Controller (MVC) pattern. Astro apps use controllers to manage some portion of the screen (the V in MVC). Sometimes it's also useful to package some non-visual feature into a controller such as deep link management.

Controllers provide you with a way to organize your code based on behaviour. For example, in many Astro apps we have a TabBarController which manages the tab bar, the screens displayed when tapping on tabs, and responding to events generated when the user interacts with the tab bar. This removes most of the tab bar management code from the main app.js file and provides a single place to look when you need to modify how the tab bar works in an app.

Defining a Controller

Controllers in Astro are defined as Common JS modules. CommonJS modules are great because they allow us to easily define the dependencies the module needs and encapsulate our module so that nothing bleeds into the global namespace. Let's create a file named tabController.js and put the following into it:

var TabController = function() {};

module.exports = TabController;

This defines a module named TabController (as a constructor function) and then returns it. This constructor function becomes the actual module we will build out in the rest of this lesson.

You can import any dependencies the controller needs by using the require() function. Let's make this controller depend on Astro and one plugin, the TabBarPlugin.

var Astro = require('astro/astro-full');
var TabBarPlugin = require('astro/plugins/tabBarPlugin');
var TabController = function() {};

module.exports = TabController;

Notice how we use require() to import the two modules this controller depends on. All Astro components that you need are always prefixed with astro/. Module dependencies are always specified as the module's js filename without the trailing .js extension.

Initialization

Every controller should expose a "static" module method named init which takes care of creating the controller. We use this pattern instead of invoking the constructor function directly because all plugins in Astro follow the same init pattern and are created asynchronously. This pattern allows us to return a promise from the init() function and then wait on it to make sure the controller is ready for use before we start calling methods on it.

For example, we would use the controller like this:

var controllerPromise = TabController.init();
controllerPromise.then(function(controller) {
    // Now we can call methods on the controller object...
});

Let's add the init method to our controller by adding the following code right after the constructor function.

var Astro = require('astro/astro-full');
var TabBarPlugin = require('astro/plugins/tabBarPlugin');
var TabController = function() {};

TabController.init = function() {
    return Promise.resolve(new TabController());
};

module.exports = TabController;

This init method instantiates a new TabController object, wraps it in a promise, and returns it.

NOTE: At this point in the tutorial we aren't calling any async methods in the init method. Because of this we're just wrapping the new controller object in a promise to comply with our convention that all init methods return a promise. If we didn't, someone who expects it to return a promise would get an error when they call .then() on the result of init().

Exposing a View

Controllers that manage some part of the screen should expose one, and only one, view to other parts of the app. This allows other controllers, or app.js, to put the controller's view into some part of the screen. So in app.js you would create this controller, take the view it exposes, and add it to an AnchoredLayoutPlugin or set it as the Application's main view.

In Astro apps we use a convention of calling the view exposed on the plugin the viewPlugin. Our controller will manage any functionality that relates to its viewPlugin (good encapsulation). Code outside of a controller should never invoke methods on that controller's viewPlugin. External code should also not make assumptions about what type a controller's viewPlugin is. It should only assume that a controller's viewPlugin is actually a view and it can be placed into the view hierarchy somewhere.

Let's create a view, an TabBarPlugin, and make it our controller's view:

var Astro = require('astro/astro-full');
var TabBarPlugin = require('astro/plugins/tabBarPlugin');

var TabController = function(tabBar) {
    this.viewPlugin = tabBar;
};

TabController.init = function() {
    return TabBarPlugin.init().then(tabBar) {
        return new TabController(tabBar);
    });
};

module.exports = TabController;

Notice a few things here. We've created a TabBarPlugin using its init method, waited for it to resolve using the promise's then() method and then passed it to the constructor. We've also set the tabBar parameter as the viewPlugin for the controller.

At this point we could instantiate the TabController module and place it's view into the view hierarchy somewhere. For example in app.js we could do this:

var Astro = require('astro/astro-full');
var TabController = require('./tabController);

TabController.init().then(function(tabController) {
    Astro.setMainViewPlugin(tabController.viewPlugin);
});

Custom Behaviour

Now that we have a controller module we can add custom behaviour to it. For this discussion, let's say the controller that manages the tab bar needs to be able to switch to a given tab when the user actions a deep link. Let's expose a method that allows app.js to select the first tab when it receives the deep link event.

var Astro = require('astro/astro-full');
var TabBarPlugin = require('astro/plugins/tabBarPlugin');

var TabController = function(tabBar) {
    this.viewPlugin = tabBar;
};

TabController.init = function() {
    return TabBarPlugin.init().then(tabBar) {
        return new TabController(tabBar);
    });
};

TabController.prototype.selectTab(tabIndex) {
    // Look up itemID based on tab index
    var itemId = tabItems[tabIndex].id;
    this.viewPlugin.selectItem(itemId);
}

module.exports = TabController;

Composition of Controllers

Let's wrap up this discussion of controllers by looking at how you can compose them. Sometimes a single screen might be complex. Instead of making a controller that is very long, you can break the screen into multiple controllers managing smaller pieces of the screen and then compose them. This composition is flexible and lets you break things up in a way that makes sense for your project.

A good example is when you are building an app with a tab bar. You'll want to have a controller managing the tab bar and the main views for each tab. But within those main views (per tab) it will become unwieldy to have all of the code for each tab in the main tab controller. This is a good opportunity to have one controller per tab.

To get a feel for how this works let's assume that there are two already-created tab controllers named DiscoverTabController and AccountTabController. These manage the views and behaviors for two tabs in your app. When the user taps the "Discover" tab, we will display the DiscoverTabController and when they tap the "Account" tab, we will display the AccountTabController. Let's extend our TabController to do this.

var Astro = require('astro/astro-full');
var AnchoredLayoutPlugin = require('astro/plugins/anchoredLayoutPlugin');
var TabBarPlugin = require('astro/plugins/tabBarPlugin');
var DiscoverTabController = require('./discoverTabController');
var AccountTabController = require('./accountTabController');

var TabController = function(anchoredLayout, tabBar, discoverTab, accountTab) {
    this.viewPlugin = anchoredLayout;
    this.tabBar = tabBar;
    this.discoverTab = discoverTab;
    this.accountTab = accountTab;
    this.viewPlugin.addBottomView(tabBar);

    // Start out on the discover tab
    this.selectTab(0);
};

TabController.init = function() {
    return Promise.join(
        AnchoredLayoutPlugin.init(),
        TabBarPlugin.init(),
        DiscoverTabController.init(),
        AccountTabController.init(),
        function(
            anchoredLayout,
            tabBar,
            discoverTab,
            accountTab
        ) {
            return new TabController(
                anchoredLayout,
                tabBar,
                discoverTab,
                accountTab
            );
    });
};

TabController.prototype.selectTab(tabIndex) {
    switch(tabIndex) {
        case 0:
            this.viewPlugin.setContentView(this.discoverTab.viewPlugin);
            break;
        case 1:
            this.viewPlugin.setContentView(this.accountTab.viewPlugin);
            break;
    }
}

module.exports = TabController;

Whew, that's quite a bit of code! Let's work through it. The init method has changed quite a bit. It now creates an AnchoredLayoutPlugin so that we can display the tab bar at the bottom of the screen and have a content area for the rest of the screen. We then hook up the selectTab method to swap in the appropriate view given the tabIndex.

This is controller composition at work! Each tab is managed by a specific controller and we have our TabController managing switching between them. Now as you add behaviour to each of the tabs, you have a place to put it without it polluting the TabController module.

Summary

In this section you learned about controllers and how they're used in Astro. Controllers provide a way to encapsulate different parts of your app. You saw how controllers expose one, and only one view, and can be composed together to build out complex apps.