Routing to a Controller with no View in Angular

Posted by Rick Strahl on West-Wind See other posts from West-Wind or by Rick Strahl
Published on Tue, 15 Oct 2013 20:56:01 GMT Indexed on 2013/10/17 15:56 UTC
Read the original article Hit count: 581

Filed under:

I've finally had some time to put Angular to use this week in a small project I'm working on for fun. Angular's routing is great and makes it real easy to map URL routes to controllers and model data into views. But what if you don't actually need a view, if you effectively need a headless controller that just runs code, but doesn't render a view?

Preserve the View

When Angular navigates a route and and presents a new view, it loads the controller and then renders the view from scratch. Views are not cached or stored, but displayed and then removed. So if you have routes configured like this:

'use strict';

// Declare app level module which depends on filters, and services
window.myApp = angular.module('myApp', ['myApp.filters', 'myApp.services', 'myApp.directives', 'myApp.controllers']).
  config(['$routeProvider', function($routeProvider) {
      $routeProvider.when('/map',
          {
              template: "partials/map.html ", 
controller: 'mapController', reloadOnSearch: false, animation: 'slide' });
$routeProvider.otherwise({redirectTo: '/map'}); }]);

Angular routes to the mapController and then re-renders the map.html template with the new data from the $scope filled in.

But, but… I don't want a new View!

Now in most cases this works just fine. If I'm rendering plain DOM content, or textboxes in a form interface that is all fine and dandy - it's perfectly fine to completely re-render the UI.

But in some cases, the UI that's being managed has state and shouldn't be redrawn. In this case the main page in question has a Google Map on it. The map is  going to be manipulated throughout the lifetime of the application and the rest of the pages.

In my application I have a toolbar on the bottom and the rest of the content is replaced/switched out by the Angular Views:

geocrumbs

The problem is that the map shouldn't be redrawn each time the Location view is activated. It should maintain its state, such as the current position selected (which can move), and shouldn't redraw due to the overhead of re-rendering the initial map.

Originally I set up the map, exactly like all my other views - as a partial, that is rendered with a separate file, but that didn't work.

The Workaround - Controller Only Routes

The workaround for this goes decidedly against Angular's way of doing things:

  • Setting up a Template-less Route
  • In-lining the map view directly into the main page
  • Hiding and showing the map view manually

Let's see how this works.

Controller Only Route

The template-less route is basically a route that doesn't have any template to render. This is not directly supported by Angular, but thankfully easy to fake. The end goal here is that I want to simply have the Controller fire and then have the controller manage the display of the already active view by hiding and showing the map and any other view content, in effect bypassing Angular's view display management.

In short - I want a controller action, but no view rendering.

The controller-only or template-less route looks like this:

      $routeProvider.when('/map',
          {
              template: " ", // just fire controller
              controller: 'mapController',              
              animation: 'slide'
          });

Notice I'm using the template property rather than templateUrl (used in the first example above), which allows specifying a string template, and leaving it blank. The template property basically allows you to provide a templated string using Angular's HandleBar like binding syntax which can be useful at times.

You can use plain strings or strings with template code in the template, or as I'm doing here a blank string to essentially fake 'just clear the view'.

In-lined View

So if there's no view where does the HTML go?

Because I don't want Angular to manage the view the map markup is in-lined directly into the page. So instead of rendering the map into the Angular view container, the content is simply set up as inline HTML to display as a sibling to the view container.

<div id="MapContent" data-icon="LocationIcon"
        ng-controller="mapController" style="display:none">
    <div class="headerbar">
        <div class="right-header" style="float:right">
            <a id="btnShowSaveLocationDialog"
                class="iconbutton btn btn-sm"
                href="#/saveLocation" style="margin-right: 2px;">
                <i class="icon-ok icon-2x" style="color: lightgreen; "></i>
                Save Location
            </a>
        </div>
        <div class="left-header">GeoCrumbs</div>
    </div>
    <div class="clearfix"></div>

    <div id="Message">
        <i id="MessageIcon"></i>
        <span id="MessageText"></span>
    </div>

    <div id="Map" class="content-area">
    </div>
</div>


<div id="ViewPlaceholder" ng-view></div>

Note that there's the #MapContent element and the #ViewPlaceHolder. The #MapContent is my static map view that is always 'live' and is initially hidden. It is initially hidden and doesn't get made visible until the MapController controller activates it which does the initial rendering of the map. After that the element is persisted with the map data already loaded and any future access only updates the map with new locations/pins etc.

Note that default route is assigned to the mapController, which means that the mapController is fired right as the page loads, which is actually a good thing in this case, as the map is the cornerstone of this app that is manipulated by some of the other controllers/views.

The Controller handles some UI

Since there's effectively no view activation with the template-less route, the controller unfortunately has to take over some UI interaction directly. Specifically it has to swap the hidden state between the map and any of the other views.

Here's what the controller looks like:

myApp.controller('mapController', ["$scope", "$routeParams", "locationData",
    function($scope, $routeParams, locationData) {

        $scope.locationData = locationData.location;
        $scope.locationHistory = locationData.locationHistory;
        if ($routeParams.mode == "currentLocation") {
            bc.getCurrentLocation(false);
        }

        bc.showMap(false,"#LocationIcon");
        
    }]);

bc.showMap is responsible for a couple of display tasks that hide/show the views/map and for activating/deactivating icons. The code looks like this:

this.showMap = function (hide,selActiveIcon) {
    if (!hide)
        $("#MapContent").show();
    else {
        $("#MapContent").hide();            
    }
    self.fitContent();

    if (selActiveIcon) {
        $(".iconbutton").removeClass("active");
        $(selActiveIcon).addClass("active");
    }

};

Each of the other controllers in the app also call this function when they are activated to basically hide the map and make the View Content area visible. The map controller makes the map.

This is UI code and calling this sort of thing from controllers is generally not recommended, but I couldn't figure out a way using directives to make this work any more easily than this. It'd be easy to hide and show the map and view container using a flag an ng-show, but it gets tricky because of scoping of the $scope. I would have to resort to storing this setting on the $rootscope which I try to avoid. The same issues exists with the icons.

It sure would be nice if Angular had a way to explicitly specify that a View shouldn't be destroyed when another view is activated, so currently this workaround is required. Searching around, I saw a number of whacky hacks to get around this, but this solution I'm using here seems much easier than any of that I could dig up even if it doesn't quite fit the 'Angular way'.

Angular nice, until it's not

Overall I really like Angular and the way it works although it took me a bit of time to get my head around how all the pieces fit together. Once I got the idea how the app/routes, the controllers and views snap together, putting together Angular pages becomes fairly straightforward. You can get quite a bit done never going beyond those basics. For most common things Angular's default routing and view presentation works very well.

But, when you do something a bit more complex, where there are multiple dependencies or as in this case where Angular doesn't appear to support a feature that's absolutely necessary, you're on your own. Finding information on more advanced topics is not trivial especially since versions are changing so rapidly and the low level behaviors are changing frequently so finding something that works is often an exercise in trial and error.

Not that this is surprising. Angular is a complex piece of kit as are all the frameworks that try to hack JavaScript into submission to do something that it was really never designed to. After all everything about a framework like Angular is an elaborate hack. A lot of shit has to happen to make this all work together and at that Angular (and Ember, Durandel etc.) are pretty amazing pieces of JavaScript code. So no harm, no foul, but I just can't help feeling like working in toy sandbox at times :-)

© Rick Strahl, West Wind Technologies, 2005-2013
Posted in Angular  JavaScript  

© West-Wind or respective owner