Introduction
This tip describes how to write JavaScript code which performs UI manipulations on the view side, without polluting controller code.
Background
Occasionally, everyone has bad luck to have to manipulate DOM
imperatively, based on business logic. It’s a good idea to keep this code
away from the controller. Problem comes up when you need access to
controllers scope in UI code as soon as it’s created.
There are few rules which need to be followed in order to get the job done AND keep clean separation of concerns:
- Get access to controller scope as soon as possible
- Don’t make changes to controller which introduce dependency on UI
code (it would be possible to call global function from a controller to
inform about the event)
- Don’t create any global variables
Using the Code
A way to do this (admittedly, ugly way) is to use ngIf directive.
I marked controllers element with an ID so I can find its scope
later, and aliased controller name to capture controller reference in a
variable. I’ve called it vm
here, as in “view-model”.
<div ng-controller="MyController as vm" id="myView">
Then I added script
element where I can place UI manipulation code and applied ngIf directive to it. Parameter of ngIf
directive is the controller.
<script type="text/javascript" ng-if="vm">
var view = angular.element('#myView');
if (view.hasClass('ng-scope')){
$scope = view.scope();
function viewManipulator($rootScope){
alert('Controller has loaded');
$scope.$watch('Name', function (newValue, oldValue){
if (!newName)
return;
alert('Hello ' + newValue + '!');
});
}
viewManipulator['$inject'] = ['$rootScope'];
view.injector().invoke(viewManipulator);
}
</script>
viewManipulator
will be called right after controller is loaded and
therefore scope is created and alert will show up. There, we can hook up
watches over scope data.
How Does This Work?
Key is in lines 1 and 3. ngIf
directive manipulates DOM to remove and add elements depending on
the provided expression. The first time that browser loads the script, the
script gets executed. Controller is not yet created at this point. For
that reason we have line 3, which checks for magic AngularJS class. ng-scope
class is a special class that AngularJS applies to elements which have
their own scope created (for example, controller elements). First time
that line 3 executes, controller is not created and ng-scope
class is not applied.
AngularJS continues initializing the controller. Before MyController
controller is fully created, ngIf
directive on our script
element
removes the script
(because our controller does not exist yet). After
creation of controller is finished, ngIf
is re-evaluated, vm
is
available and script
element is added to DOM again. Because script
is
added, it is executed again, but this time line 3 evaluates to true
so our UI manipulation code can be executed this time.
I’ve injected $rootScope
just to illustrate that we can also get dependency injection. It’s not needed in this example. $scope
cannot be injected, so we are closing over variable in which we captured it. Other services can be injected.
You can see a demonstration of the hack in this plunk.
Buttons modify model in controller code, but UI is changed from view
code which is hooked up to controllers scope. I can already hear some of
you say “But you can do this with a filter!”. Yes, I can do what I
demonstrated with a filter, but this is just a simplified example.
Actual scenarios are not always so simple. Sometimes calculations need
to be performed and complex decisions need to be made when changing the
UI.
I hope this is useful to someone, and that I didn’t break too many rules with this approach.
History