Occasionally everyone has bad luck to have to manipulate DOM imperatively, based on business logic. It’s 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
A way to do it (admittedly, ugly way) is to use ngIf directive.
I marked controllers element with an ID so I can find it’s 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.
viewManipulator will be called right after controller is loaded and therefor scope is created and alert will show up. There we can hook up watches over scope data.
Why does this work?
Key is in lines 1 and 3. ngIf directive manipulates DOM to remove and add elements depending on 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.
Hope this is useful to someone, and that I didn’t break too many rules with this approach.