Contact   •   Products   •   Search

Rick Strahl's Web Log

Wind, waves, code and everything in between...
ASP.NET • C# • HTML5 • JavaScript • AngularJs

Routing to a Controller with no View in Angular


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 :-)

Make Donation
Posted in Angular  JavaScript  


Feedback for this Post

 
# re: Routing to a Controller with no View in Angular
by Alex October 19, 2013 @ 7:11pm
Rick, are you using Angular for SPA with .NET?
I still can't believe VS Team didn't ship any decent SPA template in VS2013. All these manual Javascript without compile check is sort of a rollback in development, don't you think? And so many packages... Some call it Javascript hell (as was dll hell before).
# re: Routing to a Controller with no View in Angular
by Johan October 20, 2013 @ 4:09pm
Not sure why you need routes. Just use your map as the main template (which you effectively have already) and then overlay the menu selection "views" using css and conditionals like ng-switch / ng-if / ng-show / ng-hide. The benefit of switch/if is the elements are added/removed from DOM as required.
# re: Routing to a Controller with no View in Angular
by Adrian Hara October 21, 2013 @ 2:00am
You might want to take a look at Angular UI Router: https://github.com/angular-ui/ui-router

It basically defines the app as a state machine, wich each state declaring its visual composition. Using it would make what you're trying to do easy and (relatively) clear (I guess a state for the map plus a substate for the actions bar would do the trick).
# re: Routing to a Controller with no View in Angular
by Matt Schick June 02, 2014 @ 11:23am
@Adrian Hara

Angular UI router only solves half of the problem, where it falls down is that it will always rerender any of the views, causing that map to re-render each time. They actually spent awhile considering implementing some type of view caching, but eventually decided not to.

https://github.com/angular-ui/ui-router/issues/63
 


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2014