Same list of items in the parent's controller
Imagine you have an Angular controller showing a list of items using ng-repeat
directive.
If there is another controller it can simply show the same list - by default the parent's scope
is either reused or a prototype of the child's scope.
The HTML template is simple
1 | <div class="parent" ng-controller="Parent"> |
The application code
1 | angular.module('App', []) |
Adding new item to the list
We might want to edit the list of strings, for example by adding new items.
- The entered item is added in the parent controller and the changes are reflected instantly
in the child controller. The child's
$scope.items
is the same as the parent's$scope.items
because of scope inheritance.
1 | angular.module('App', []) |
Use directives instead of controllers
While preparing for the future and to better isolate the components, let us rewrite controllers to be Angular directives.
1 | <body ng-app="App"> |
The parent directive can even update the list of items periodically.
1 | angular.module('App', []) |
Every 3 seconds a new item will be pushed into the array. Notice that both lists are updated.
We can confirm that the list reference is the same in both directives. From the browser console execute this code to check
1 | var parentScope = angular.element($('.parent')).isolateScope(); |
Separate child's scope from the parent
Let us go one step further. We can make the child's scope isolate too, explicitly passing only
the items
list reference.
1 | angular.module('App', []) |
The list updates every 3 seconds, just like before
Notice that we now overwrite the $scope.items
array (after adding new item)
1 | function addItem() { |
Yet, both components stay in sync. This is because the Angular's digest cycle noticed the change and copied the new array reference from the parent to the child scope. We can confirm this but executing the following code
1 | var parentScope = angular.element($('.parent')).isolateScope(); |
Every 3 seconds, the parent's scope replaces the $scope.items
with a new value.
After $interval
is finished, the digest cycle runs and synchronizes the properties mapped
from the parent's scope to the child's scope. We don't have to worry!
Move data to a service
We have refactored the controllers into directives. The next logical step is to move data from controllers into a separate service instance.
1 | angular.module('App', []) |
But this stopped working. The console clearly shows a growing list of items, but the changes are not reflected in the dome
Why? Well, the parent controller saved the first list reference as the local variable
$scope.items = Items.all;
After 3 seconds passed, inside the Items
service the reference was set to point at the new list.
But there Angular digest cycle only keeps scope properties synchronized, not services and scopes.
The parent and child directives never found out about the new reference.
1 | $interval(function () { |
It is simple to break the connection like this when refactoring the Angular code, because the digest cycle runs silently behind the scenes. Once you start refactoring, and move things away from scopes, it is easy to suddenly find yourself with a stopped data flow.
Solutions
There are several solutions for preserving the link between the Model (data in the service) and the Controller (the scope object inside the directives).
Do not store the reference at all
The first solution is to NOT store the array reference on the scope, calling the Items.all()
method
every time we need the list. Here is the relevant code changes
1 | // the service returns a method instead of the reference |
The solution works and goes well a functional programming principle: pass around behavior (functions), rather than plain data.
Never overwrite the reference in the service
We can still return the list reference, but be vigilant inside the service to NOT overwrite it
when a new list is computed. Let us go back to the plain item in items
inside the directive
1 | <h2>Parent controller</h2> |
The service fills an existing array instead of overwriting the variable
1 | function fill(destination, source) { |
The fill(destination, source)
function is used to copy items from the new array
to the existing ones, guaranteeing that the returned array is still valid.
Because the original reference never changes and the fill
takes the destination Array as
its first argument, we can apply partial binding to create a convenient method for filling
the array and never accidentally overwriting it
1 | .service('Items', function ($interval) { |
Conclusion
When you are refactoring Angular directives, remember that the properties are synchronized during the digest cycle. Just moving the code into services might break this link: the updated values in the services will not be reflected in the directives, leading to an interesting debug experience. If you return a reference to an Array from a service, assume that you need to always place new values into that Array and not create a new one.