How to do OO in AngularJS

09 Jul 2014

How to do OO in AngularJS

Strangely, while enjoying the convenience brought by angular's View-Controller model, I sub-consciously lost the ability of doing OO in a lot of places where I should. Think about the following case.

I have a hierarchy of directives which are designed to handle form inputs, they individually have the ability to do validations, listening to special events, etc. And, some behaviors are shared among them.

Let's say I have three following 3 directives handling form inputs

huula-email
huula-postcode
huula-phone-number
all of them need to watch the user input and show an alert sign if the input is invalid. How do we achieve that? Usually, this is how you create the huula-email directive. Note that all code are in CoffeeScript, but you can do the same with JavaScript, only more verbose. Just for clarifying ideas, don't take it literally.
myDirectives.directive('huulaEmail', [ ->
  return {
    restrict: 'E'
    templateUrl: "[path-to-template.html]"
    scope: {
      m: "=ngModel"
    }
    require: 'ngModel'
    controller: ['$scope', ($s) ->
      $s.pattern = /^[-0-9a-zA-Z.+_]+@[-0-9a-zA-Z.+_]+\.[a-zA-Z]{2,4}$/

      # validation starts
      $s.valid = true
      $s.$watch('m.value', (newVal, oldVal) ->
        $s.valid = true
        if not $s.pattern.test(newVal)
          $s.valid = false
      )
      # validation ends

    ]
  }
])

For the other two directives huula-postcode and huula-phone-number, the only difference among them is the pattern. Naturally, we starts thinking of some kind of base Controller that encapsulates the validation logic. How to do that in AngularJS? Well, we can actually use the prototype inheritance of JS here. Although it's sometimes not straight forward under the context of AngularJS and CoffeeScript. It's actually a great idea. Our solution are based on the following premises.

Premise 1. Angular calls new on the function you provide through controller.
Premise 2. Any function in JS can inherit from a parent function.

Rule 1 is very important here coz we are using prototype inheritance. Normally rule 1, 2 suffices here, but as we are using CoffeScript, we also need to confirm that some syntax of CoffeeScript works for us. After some investigation, it does! (You can look into the generated code if you like). So here is what you can use in your app now.

class BaseController
  contructor: ($s, pattern) ->
    $s.pattern = pattern

    # validation starts
    $s.valid = true
    $s.$watch('m.value', (newVal, oldVal) ->
      $s.valid = true
      if not $s.pattern.test(newVal)
        $s.valid = false
    )
    # validation ends


myDirectives.directive('huulaEmail', [ ->
  return {
    restrict: 'E'
    templateUrl: "[path-to-template.html]"
    scope: {
      m: "=ngModel"
    }
    require: 'ngModel'
    controller: ['$scope', class EmailController extends BaseController
      contructor: ($s) ->
        super($s, /^[-0-9a-zA-Z.+_]+@[-0-9a-zA-Z.+_]+\.[a-zA-Z]{2,4}$/)
    ]
  }
])

myDirectives.directive('huulaPostcode', [ ->
  return {
    restrict: 'E'
    templateUrl: "[path-to-template.html]"
    scope: {
      m: "=ngModel"
    }
    require: 'ngModel'
    controller: ['$scope', class EmailController extends BaseController
      contructor: ($s) ->
        super($s, /^\d{6}$/)
    ]
  }
])

That's it, now you don't need to duplicate the validation code!

But wait, can we abstract this further? Some smart reader probably notice that there are still some boilerplates in the directive literal object. You would probably think that we can use the same thing we have done for controller, but actually you cannot do it here. Because the first premise doesn't hold for directives. Angular just calls the function of a directive other than instantiates it. In order to eliminate the boilerplate here, you can utilize the angular.extend and just manipulate on the object literal instead.