Angular JS: Navigator Pattern

New To AngularJS?

Not sure what Angular JS is? 

The short version is that it is a client-side Javascript library.  And unlike most of those libraries I’ve fooled around with, it’s very quick to get started with, and makes a lot of stuff really easy.  Pulling data from JSON REST services.  Hash-bang routing.  Two-way data-binding.  All crazy simple.

To quote one fella a few months ago: “It’s like a backbonejs and knockoutjs sandwich covered in awesomesauce."

The website is here: http://angularjs.org/

The best point-by-point introduction you can get is here: http://www.egghead.io/

The best “let’s actually build something” introduction you can get is here: http://tekpub.com/products/angular

Even if you’ve worked in Angular JS in the past, go through the egghead.io and TekPub videos anyway.  Tons of good info in both of them.

The Problem

Back already?  OK, so now that you are familiar Angular JS, and maybe you’ve built an application or two with it. 

As you build out your applications, you’ll inevitably encounter some scaling problems.  You  are probably running into big fat controllers, hundreds of JS and HTML template files, and the like.  Any there are plenty of resources just a google away on how to deal with them. 

But the one problem I have not seen dealt with (to my satisfaction at least) is how to deal with the client side URLs.  Angular’s router makes it so easy to navigate to a URL have it bind to a specific controller and view.  But what happens when those URLs start to have a few parameters?  What happens when you want to change them?  All of the sample code I’ve seen has the URLs defined in the router config, and in the HTML templates, and maybe in the controllers as well.  Hard-coded, manually-concatenated strings all over the place.  Arg.

I don’t like this, and I want a better solution.

What Are You Talking About?

(The code samples for this are from the Sriracha Deployment System, and open source deployment platform that I’m building for fun, and the latest code is here: https://github.com/mmooney/Sriracha.Deploy)

So for example, let’s pretend you want to display project details.  You’ll create an HTML template that looks something like this:

<h2>
    Project {{project.projectName}} 
    <a ng-href="#/project/edit/{{project.id}}">
        (edit)
    </a>
</h2> 
... more info here ...

And then you would define your router like this:

ngSriracha.config(function ($routeProvider) {
    $routeProvider
        .when("/project/:projectId", {
            templateUrl: "templates/project-view-template.html",
            controller: "ProjectController"
        })

And then later when someone wants to get to that page, you direct them you create a link to “/#/project/whateverTheProjectIdIs”:

<tr ng-repeat="project in projectList">
    <td>
        <a ng-href="/#/project/{{project.id)}}">
            {{project.projectName}}
        </a>
    </td>
</tr>

 

OK, now we have three separate places that we’re referencing the URL, and they are all slightly different, and we have not even really built anything yet.  As this turns into a real application, we’re going to have 20 or 30 or 50 variations of this URL all over the place.  And at least 5 of them will have a typo.  And you will be sad.  Very sad, I say.

If we wanted to change this URL from “/project/:projectId” to “/project/details/:projectId”, it’s going to be a huge pain  the neck.  Couple this with the inevitable hard-to-find bugs that you’re going encounter because you spelled it “/proiect” instead of “/project”, and you’re wasting all kinds of time.

First Attempt, An Incomplete Solution

So I when through a few attempts at solving this before I settled on something that really worked for me.

First things first, I built a navigation class to store the URLs:

var Sriracha = Sriracha || {};

Sriracha.Navigation = {
    HomeUrl: "/#",
    Home: function () {
        window.location.href = this.HomeUrl;
    },

    GetUrl: function (clientUrl, parameters) {
        var url = clientUrl;
        if (parameters) {
            for (var paramName in parameters) {
                url = url.replace(":" + paramName, parameters[paramName]);
            }
        }
        return "/#" + url;
    },

    GoTo: function (clientUrl, parameters) {
        var url = this.GetUrl(clientUrl, parameters);
        window.location.href = url;
    },

    Project : {
        ListUrl: "/",
        List: function () {
            Sriracha.Navigation.GoTo(this.ListUrl);
        },
        CreateUrl: "/project/create",
        Create: function () {
            Sriracha.Navigation.GoTo(this.CreateUrl)
        },
        ViewUrl: "/project/:projectId",
        View: function (id) {
            Sriracha.Navigation.GoTo(this.ViewUrl, { projectId: id });
        },

 

At least now when I needed a URL from Javascript, I could just say Sriracha.Navigation.Project.ViewUrl.  When I want to actually go to a URL, it would be Sriracha.Navigation.Project.View(project.id);.  And if I wanted the formatted client side URL, with leading /# included and the parameters included, I could use the GetUrl() function to format it.  You’re router config is a little less hard-coded:

ngSriracha.config(function ($routeProvider) {
    $routeProvider
        .when(Sriracha.Navigation.Project.ViewUrl, {
            templateUrl: "templates/project-view-template.html",
            controller: "ProjectController"
        })

 

This worked pretty good, except for HTML templates.  This is a bit crazy to be burning into your ng-href calls, plus your templates should really only isolate their concerns tot he $scope object.  I’m pretty sure the Dependency Injection Police would come hunt me down with pitchforks if I start calling static global Javascript objects from inside a HTML template.  Instead I ended up with a lot of this:

<tr ng-repeat="project in projectList">
    <td>
            <a ng-href="{{getProjectViewUrl(project.id)}}">
                {{project.projectName}}
            </a>
    </td>
</tr>

 

And then this:

$scope.getViewProjectUrl = function (project) {
    if (component) {
       return Sriracha.Navigation.GetUrl(Sriracha.Navigation.Project.ViewUrl, { projectId: project.id });
    }
}

 

More Arg.

Better Solution: Navigator Pattern

So I figured what you really want is an object, that can be injected into the router configuration, and also into controller, and then stored in the scope so that it can be referenced by the HTML templates.

I played with Service and Factory and all of that jazz, but really if you want to create an object that is going to get injected into the router config, you need a Provider. 

So I created an object that looks just like the static global SrirachaNavigation object I had before, but a little more suited for the ways I want to use it.

ngSriracha.provider("SrirachaNavigator", function () {
    this.$get = function () {
        var root = {
            getUrl: function (clientUrl, parameters) {
                var url = clientUrl;
                if (parameters) {
                    for (var paramName in parameters) {
                        url = url.replace(":" + paramName, parameters[paramName]);
                    }
                }
                return "/#" + url;
            },
            goTo: function (clientUrl, parameters) {
                var url = this.getUrl(clientUrl, parameters);
                window.location.href = url;
            }
        };
        root.project = {
            list: {
                url: "/project",
                clientUrl: function () { return root.getUrl(this.url) },
                go: function() { root.goTo(this.url) }
            },
            create: {
                url: "/project/create",
                clientUrl: function () { return root.getUrl(this.url) },
                go: function() { root.goTo(this.url) }
            },
            view: {
                url: "/project/:projectId",
                clientUrl: function (projectId) { return root.getUrl(this.url, { projectId: projectId }) },
                go: function(projectId) { root.goTo(this.url, { projectId: projectId}) }
            },

Now we can inject this into the router config and use that instead:

ngSriracha.config(function ($routeProvider, SrirachaNavigatorProvider) {
    var navigator = SrirachaNavigatorProvider.$get();
    $routeProvider
        .when(navigator.project.view.url, {
            templateUrl: "templates/project-view-template.html",
            controller: "ProjectController"
        })

 

And then inject it into our controllers:

ngSriracha.controller("ProjectController", function ($scope, $routeParams, SrirachaResource, SrirachaNavigator) {
    $scope.navigator = SrirachaNavigator;

And then when we need to reference a URL from the your code:

<tr ng-repeat="project in projectList">
    <td>
        <a ng-href="{{navigator.project.view.clientUrl(project.id)}}">
            {{project.projectName}}
        </a>
    </td>
</tr>

Much, much nicer in my opinion.

 

But, but but…

I know, you may think this is silly. 

If you don’t have a problem putting your URLs all over the place, good for you, stick with what works for you.

Or, if you think this is an absurd waste of effort, because there is a better generally accepted way to deal with this, awesome.  Please let me know what you’re doing.  I’ve love to toss this and use a better way, I just have not been able to find anyone else who’s really solved this yet (to my great surprise).

But until then, if you’re AngularJS URLs are getting you down, this might work for you.

Good luck.

9 thoughts on “Angular JS: Navigator Pattern

  1. Capaj

    I still think it is better to just really think and map out all of your web app before you start coding. Think about the possible additions of new features/views. Hardcoding urls does not seem like such a big problem, if you won’t be changing it in the future. And you should not-don’t forget that links to your page will not work, if you change your routes.

    Reply
  2. Mike Mooney Post author

    Capaj, I agree that you don’t want to be changing your URLs once you have your application in place, and I try to think through how my application is going to be laid out, but I don’t want that to be set in stone until the last possible moment. Like anything else, I try to start with a general blueprint, but then interatively build out refactor as I go. I know that the first 5 things I build are going to be completely stupid and wrong, so I really want to have a flexible tool that lets me correct those design mistakes before it’s too late.

    Reply
  3. Mike Mooney Post author

    Hi Nick and Matt, thanks for the info on those projects, I’ll definitely take a look.

    One of the main reason I wrote this to have some folks come forward and say “hey dummy, here’s a better way!”

    Reply
  4. Luis Naia

    Hi, well I also use UI-ROUTER and I’m not disappointed. But I will be moving on to ng-boilerplate ‘oh so very soon’ in the next couple of days.

    If you consider your development of a SPA Angular app, as a compiled app, and all that it implies, then you are good to go.
    Taking into account that you use a server and templates, to generate your ‘hard coded URL’. Yeah might be overkill, but in full blown apps, things will get messy sooner than latter, when requirements and implementations start to change. So this time i’m following advice from third party: http://blog.artlogic.com/2013/05/02/ive-been-doing-it-wrong-part-1-of-3/
    And going in another direction.

    Since your app development is now ‘compile and run’ it has all that extra work at beginning, but also speeds up a lot of other things.
    Keep us up to
    date!
    https://github.com/joshdmiller/ng-boilerplate

    Reply
  5. Pingback: AngularJS Highlights: Week Ending 18 August 2013 | SyntaxSpectrum

  6. Pingback: AngularJS – Resource collection I | duetsch.info

Leave a Reply to Pete Martin Cancel reply

Your email address will not be published.