Abstract

Here I show how to use AngularJS the non angular way.

Prologue

For a while I've been building an angular SPA based on 1.3.X. Most of the time I've spent on the project is actually not the angular part, but the underlying model. One of the topics was forms and form rendering.
I had to capture amounts as a user would naturally type in (eg 5,99), but in the underlying model I wanted to persist as an integer (599). For this I needed to add a data transformer to the input fields that are actually capturing amounts. In the underlying model I already had the option to add attributes dynamically, so I thought that I'll just add the attribute and angular will do the magic.
Well it did not. And after some research I understood why it did not. Having the issue understood finding the solution wasn't that big effort. However it came into my mind, that I might want to re-use - compiling a directive from a dynamic attribute - later. If I could specify a directive as an object I'd be able to extend it later.

Directives as objects

Although angular calls something directive inheritance, but is more like dependency injection in my opinion.

The very first thought I had was that angular actually lets you specify an object constructor as a controller, maybe it lets you do the same for directives. After all a directive is an object that has methods, so would be awesome if you could just do it.

Unfortunately, no you can't...

Fortunately we're talking about javascript, which means the constructor is just a simple function and calling it without the new operator works as a function call, so it will not create a new instance. Unless one tweaks it a bit!

This is referred as scope-safe constructor.

Now this constructor function can be specified for angular when creating a directive.

As you can see the directive is actually an instance of Directive.DateViewTransformer, having a property called "required" and a method called "link". Of course one could specify "controller", "controllerAs" properties to specify the directive behavior even further, just like it would be done with an anonymous object.

So why is this better then putting it into an anonymous object and passing it into the directive call?

  • First of all, the code looks much cleaner. The example directive doesn't have that much logic inside the link function, still maintaining it inside an object would be clunky.

  • Then there is the ability to actually extend the prototype of this directive to be used by another directive.

At this time I was already using controllers as objects, and after these modifications to the directives I could use directives as objects as well.

That made me curious what else could be used as objects that would have great benefits.

Routes as objects

Routes don't do too much, but they have a significant behavior which needs some attention. This is resolving dependencies. If one provides the "resolve" property to return a special object, then the properties/methods are going to be injected as dependencies which get resolved by the value the property/method returns. That sounds a bit complicated.

Let's see an example.

  
$routeProvider
    .when('/',
    {
      templateUrl: "app.html",
      controller: "AppCtrl"
      resolve: {
        app: function ($q) {
          var defer = $q.defer();
          return defer.promise;
        }
      }
    }
  )


Now imagine we have more then one dependency to be resolved. Let's assume each of them is having a medium complexity function. Getting clunky again... Let's try to come up with a better solution using Objects. To easy up things I'd like to have each and every resolve as a standalone method of the route object. To indicate that a method is actually a resolver I'll name them as "resolve[PropertyName]".

The only thing missing is actually making the object clever enough to understand that these functions are resolvers.

This can be done in the object constructor, by defining a "resolve" property, which will return an object containing all the methods that starts with "resolve", and the property value will be the route's method.

  lsRoute = function() {
  var that = this;
  Object.defineProperty(
    this,
    'resolve', {
      configurable: false,
      enumerable: true,
      get: function() {
        var methods = {};

        for (var i in that) {
          if (i.search(/^resolve(.+)$/) != -1 && typeof(that[i]) == 'function') {
               var match = RegExp.$1.toString();
               methods[match.charAt(0).toLowerCase() + match.slice(1)] = that[i];
          }
        }
        return methods;
      }
    }
  )
};

Now that we have a clever route object, we should just somehow define every route as an object inheriting from this base class.

Thankfully Routes are somehow special because those are not defined before run-time like directives and controllers. So there is actually a lot of room to use.

As a minimalistic approach let's now just simple create an object in the config phase as shown below:

If you need to resolve more dependencies, you just define a new "resolve[DependencyName]" method of the route object and you're good to go.

Summary

So what can we achieve with all this?
If you paid attention in the last pen you could see how thin the configuration part became. I'll just paste it in here and add the directive -as we have seen above - to demonstrate that the config part doesn't have to be verbose at all.

  var app = angular.module('app', ['ngRoute']);

app
  .config(function($routeProvider) {
    $routeProvider.when('/', new HomeRoute());
  })
  .controller('homeController', HomeController)
  .directive('dateViewTransformer', Directive.DateViewTransformer)
  ;

No inline logic at all and on the other hand everything is re-usable. Imagine we need to have an extended home route where we need to have the same dependency resolved plus an additional one. Simply extend the HomeRoute, and specify the new resolve method, and there you go.

As an example of re-using routes I'd like to share how the crud admin of my project works. For creating/updating an entity the logic is always the same:
1. create a form
2. save the form
3. redirect to success page - entity list in this case

So I have the route "create" which is just building the form and passing it to the controller. The controller handles form validation and saving.
Compared to that route "update" is almost the same. The only difference is that upon creating the form it shall be populated with data retrieved from the storage. For that I've overwritten the "resolveForm" method. Controller and template remains the same.

Directory structure

What effect does this have on the directory structure?
I like to keep things separated so I do prefer to have a structure like this:

|- module
|-- controllers
|--- homeController.js
|--- navigationController.js
|-- directives
|--- exampleDirective.js
|-- routes
|--- homeRoute.js
|-- module.js

The module configuration is in module.js - module is just a placeholder for the module name. All the other js files contain a single object. With that structure finding pieces of code is quite efficient.

Filters?

I know I haven't mentioned filters so far. Actually the reason for it is that I don't see a point using filters as anything else then functions. Also a major difference is that angular itself is expecting a function for a filter, so returning an object wouldn't make sense.
If you worry about inheritance, well you can always re-use javascript functions using 'call' or 'apply'. Inheritance makes sense if you want to re-use more then one function from a given prototype or you want to have access to properties of the parent.
None of these apply to filters which is by definition a single function.
So for filters I use a single file that contains a function rather then an object.


447 0 0