Using Route Resolves with Component-based Design in Angular

Modern software practices inherent to Angular are guiding us towards designs that are more component-based than ever before. Components allow us to simplify, organize, encapsulate, and make our code more reusable. However, component-based thinking can get complicated, especially for those familiar with fat controllers and use $scope to access data.

Making effective use of components and skinny controller design requires top to bottom design planning to take advantage of a key feature of routing engine: the resolve future.

Resolves

Both ngRoute as well as UI-Router support a feature called resolve. In route configuration, the resolve block is an area within which you can define a number of properties that contain data resolved from service calls or any other means. These resolutions can return promises, or they can return static data. The benefit in using resolves is that the routing engine takes care of waiting for promises to complete before transitioning to the desired view or state; and, if a resolution fails, it will prevent the transition from taking place.

Another benefit to using resolves is that resolve object properties in the route configuration become the names of the objects (models) injected into your view controllers. Note the distinction between models and view controllers. Models can be pure data, without functionality. View controllers, or as they would be more appropriately called if they are just data, view models, are then simply a delivery mechanism for getting data to your views…and your components.

Defining Data Resolutions

Defining resolutions for data in your routes is a very straightforward process. Both ngRoute and UI-Router support resolves in route and state configuration. The resolve block is simply a mapping between the names of injectable models, and functions that perform their resolution. You can define multiple models within a given resolve block. Each resolve function can return static data or promises.

With ngRoute, you could resolve data like so:

.when('/my/feature/:id', {
    templateUrl: '/views/my-feature/my-feature.html',
    controller: '/views/my-festure/my-feature.ctrl.js',
    controllerAs: 'vm',
    resolve: {
        myObject: ['$location', 'myService', function($location, myService) {
            return myService.get($location.search().id);
        }],
        localData: ['localStorage', function(localStorage) {
            return localStorage.getCookie('something');
        }]
    }
})

With UI-Router, it’s basically the same thing:

.state('my.feature', {
    url: '/my/feature/:id',
    templateUrl: '/views/my-feature/my-feature.html',
    controller: '/views/my-feature/my-feature.ctrl.js',
    controllerAs: 'vm',
    resolve: {
        myObject: ['$location', 'myService', function($location, myService) {
            return myService.get($location.search().id);
        }],
        localData: ['localStorage', function(localStorage) {
            return localStorage.getCookie('something');
        }]
    }
})

Using Resolved Data Models

The resolve block of a route or state defines a number of properties. The names of these properties are the names of objects that can then be injected into your view controllers. To gain access to the data resolved, simply specify the names of those objects in your controller’s function:

angular

    .module('my.features', [])
    .controller('myFeatureCtrl', myFeatureCtrl);

function myFeatureCtrl(myObject, localData) {
    // ...
}

You can continue to pass in other objects as well, if you still need access to other services, angular facilities, etc. The data resolved by your routes is simply added to the injector, it does not prevent normal dependency resolution for anything else.

Skinny Controllers, View Models & States

In a modern Angular design with skinny controllers, the controller itself, via controllerAs, becomes the ViewModel. The goal with skinny controllers is to keep them as light weight as possible. One of the best ways to do that is to inject pure data, rather than services. At that point, the view model simply becomes an aggregation of model data that’s already been resolved.

Your controllers then truly become light weight and “skinny”, serving as a data container. Your views then become true states – a momentary representation of data as it existed at a given point in time.

function myFeatureCtrl(myObject, localData) {
    var vm  = this;

    vm.myObject = myObject;
    vm.cookie = localData.cookieValue;
}

So, what do you do when you need to update the data within a view by reloading it? If views are states, then you simply reload the entire state. Supporting “states” in ngRoute is not easy, because it does not support nested views. Thus, whole views must be refreshed. This can be done by injecting $route into your view controller and either attaching it to your view model (the controller itself when using controllerAs), or by creating a refresh function on your view model:

function myFeatureCtrl($route, myObject, localData) {
    var vm = this;

    vm.myObject = myObject;
    vm.cookie = localData.cookieValue;

    vm.refresh = function() {
        $route.reload();
    }
}

And in your view (Jade template, in this case):

div.my-feature
  div // Render the object and cookie data here somehow
    button(ng-click="vm.refresh()") Refresh

Binding the button to the vm.refresh() function on click provides a way to manually refresh the state of the view.

With UI-Router, nested views give you finer control over which UI fragments get reloaded when the state of your application changes. You can refresh a view in one of two ways with UI-Router: via ui-sref links or by calling $state.reload(). The $state.reload() approach is effectively the same as with ngRoute. You could use a link tag and ui-sref to reload as well:

div.my-feature
  div // Render the object and cookie data here somehow
    a(ui-sref="my.feature({ id: vm.myObject.id})") Refresh

This is the first hurdle to overcome when using route resolves and states: think statefully. Stateful applications should be reloading states when state changes.

Components and Route Resolves

The next issue when you start working with route resolves in modern, component-based apps is getting data to your components. The first inclination of most developers is to inject the necessary services into their components, request the data, resolve the promises in their component controllers, and proceed as normal. That could work, however, what happens when you need to utilize the same data in multiple components? You could resolve the same data several times. Worse, if you are deeply composing components in, say, tables with ng-repeat, you could resolve small bits of data dozens, hundreds, or possibly, thousands of times.

Components are directives, which means they can be parameterized. With parameters, you can pass data into your components from their parents. This is a paradigm shift in constructing applications with Angular. When that shift is made, using components with resolves becomes very simple. All data, including that required by components, is resolved in the route, attached to the view model in the view controller, and passed down or transcluded into components.

Defining Parameters for your Components

Defining parameters for most components is very simple. In general, most components are likely to be elements that encapsulate some fragment of view markup. They will render a piece of a view, and thus become fully isolate scoped. With an isolate scope, defining and passing parameters is as simple as defining the isolate scope:

angular
    .module('my.components.MyObjectDetail', [])
    .directive('myObjectDetail', myObjectDetail)
    .controller('myObjectDetailCtrl', myObjectDetailCtrl);

function myObjectDetail() {
    return {
        restrict: 'E',
        templateUrl: '/components/my-object-detail/my-object-detail.html',
        controller: 'myObjectDetailCtrl',
        controllerAs: 'vm',
        scope: {
            myObject: '='
        }
    }
}

function myObjectDetailCtrl() {
}

Note that the myObject parameter, defined within the scope block, uses the = moniker. This tells Angular to compile the string passed in, allowing actual objects to be bound, rather than simply remain as strings (which would be the case with @.) With the above definition, all scope parameters will automatically attach to the view model. Normally they would simply attach to the $scope; however, since we have used controllerAs, they will attached to vm. We then have access to myObject within the components template:

div.my-object-detail
    p Id: {{vm.myObject.id}}
    p Name: {{vm.myObject.name}}

p Description: {{vm.myObject.description}}

Passing Data to your Components
Once you have parameterized your components, it becomes a simple matter of passing data to them when you use them in your views. As an aside, take care to create properly composible components to avoid the need to chain data passage. With transcludes, you can create composible components (where components are nested within each other by transcluding them at the root view level), which is generally a better approach than creating encapsulated components (where components are nested within each other directly by using more deeply nested components within the templates of higher level components).

To pass data to a component from a view, simply pass the necessary object property on the view model:

div.my-feature
  my-object-detail(my-object="vm.myObject")

That’s really all it takes to compose feature components within feature views. This allows views to be simplified, parts of them to be made reusable, and allows for all data to be pre-resolved by the route. Our final rendered composition of view and components would be something like this:

div.my-feature
  my-object-detail(my-object="vm.myObject")
      div.my-object-detail
        p Id: {{vm.myObject.id}}
        p Name: {{vm.myObject.name}}

p Description: {{vm.myObject.description}}

Composing Components While Passing Data
In our original view template, we actually had two pieces of information attached to the view model, the myObject data as well as a cookie value. If we assume that the cookie value is supposed to be rendered as part of another component, this becomes a situation where proper component composition is required. In some cases, you may wish to encapsulate child components, and in some cases encapsulation is appropriate. Such a case may be when the data required by an encapsulated child component is either the same as the data passed into it’s parent component, it’s a property of that parent component’s data, or when the data can be derived from the parent component’s data.

In many other cases, however, composition is the better approach. It is the best approach when the data required by a composed child component is dissimilar or otherwise unrelated to the data used by whatever component it is transcluded into.

In our original view, we had a button to refresh the state. We may want this refresh button to be composed within another component, say our detail component. In this contrived scenario, we need to make our detail component support this kind of composition. This can be done with transclusion:

function myObjectDetail() {
    return {
        restrict: 'E',
        templateUrl: '/components/my-object-detail/my-object-detail.html',
        controller: 'myObjectDetailCtrl',
        controllerAs: 'vm',
        scope: {
            myObject: '='
        },
        transclude: true
    }
}

This requires a change in our template for the component, as well. We must define where the transcluded content will go:

div.my-object-detail
    p Id: {{vm.myObject.id}}
    p Name: {{vm.myObject.name}}
    p Description: {{vm.myObject.description}}
div(ng-transclude)

We could then update our view as follows to compose our refresh button into our detail component:

div.my-feature
  my-object-detail(my-object="vm.myObject")
    button(ng-click="vm.refresh()") Refresh

Our final rendered composition would then end up something like the following:

div.my-feature
  my-object-detail(my-object="vm.myObject")
      div.my-object-detail
        p Id: {{vm.myObject.id}}
        p Name: {{vm.myObject.name}}
        p Description: {{vm.myObject.description}}
        div(ng-transclude)
            button(ng-click="vm.refresh()") Refresh

Passing Handlers to Components

In the above example, we transcluded a button that was bound to a higher level scope, and called the refresh() method on the view model. Transclusion is certainly an option for such a circumstance. However, in situations when you always want the same markup for the my-object-detail component, a transclude is not the best option. The use case with a button and a click handler is unique from the more common use case where another component that renders dissimilar data may be transcluded, especially if the transclusion is not always the same for each use case of the my-object-detail component.

In the case of passing a handler, sometimes it is best to actually use a function binding parameter in your component’s directive. This allows a handler to be bound and called from within the component, thus supporting a consistent markup for the template.

function myObjectDetail() {
    return {
        restrict: 'E',
        templateUrl: '/components/my-object-detail/my-object-detail.html',
        controller: 'myObjectDetailCtrl',
        controllerAs: 'vm',
        scope: {
            myObject: '=',
            onRefresh: '&'
        }
    }
}

The onRefresh parameter uses the & moniker rather than @ (string passing) or = (data binding). This moniker represents a function binding. As such, we can pass to our component a handler to be called “on refresh”. Our components markup could the be modified to permanently encapsulate the refresh button:

  div.my-object-detail
    p Id: {{vm.myObject.id}}
    p Name: {{vm.myObject.name}}
    p Description: {{vm.myObject.description}}
    button(ng-click="vm.onRefresh()") Refresh

The button’s ng-click is now bound to the onRefresh parameter of the directive. In our view template, we must then pass in the necessary handler along with the data being rendered:

div.my-feature
  my-object-detail(my-object="vm.myObject" on-refresh="vm.refresh()")

This creates a handy, reusable object detail component with a refresh button. For any object that contains an id, name and description, we can use this same component. We can externalize the code that performs the refresh as well, this supporting different means of refreshing the state of the application if necessary. BrieBug Solutions a Denver based website and mobile application development agency that specializes in Angular and Full Stack development. If this is a technology that you would be interested in to boost your business, feel free to contact us so we can get you started on your vision!

  • Contact Us
  • Telephone: 888.679.2201
  • Address:
    BrieBug |
    12596 W Bayaud Ave Suite 201 | Lakewood, CO 80228