Forward Advance Learning

Isolate Scopes

When we create a directive we often (but not always) want it to act like it's own little application, separate from its parent. A component.

In short, we want it to have its own $scope.

Components act as mini applications inside the main application. They have well defined, explicit input and output. This means when we write them, we only need to think about the component we are working on and not the whole application. In a larger project, this helps maintain velocity and prevent code fatigue.

Angular allows us to create a directive which has an isolate scope. We can optionally pass parameters to the child scope, seeding it, and we can also optionally bind values from the document scope, so that when the document scope updates, those parameters in the child scope also update.

Creating an isolate $scope

Creating an isolate $scope in a directive is simple. Set the $scope to be an empty object, like so:

.directive('myDirective', function() {
return {
scope: {}
}
})

This will create a little application completely divorced from its parent. No matter what is on the parent $scope, the isolate $scope will be completely empty

Passing static parameters to an isolate $scope with @ (input)

We can parameterise our directive using @. We can tell our directive to take an initial value from an attribute of the directive element, like so:

.directive('streetMap', function() {
return {
scope: {
lat: '@',
lng: '@'
},
template: "<div class="map"></div>"
}
})

The @ stands for Attribute. We pass static data in via an attribute.

<street-map lat="12" lng="14"></street-map>

We may also evaluate an expression and pass in the result if we wish:

<street-map lat="{{user.lat}}" lng="{{user.lng}}"></street-map>

Passing expressions like this though is a slightly hacky way to send data into our scope. In most circumstances it is better to use 2 way binding:

Two-way isolate $scope binding with =

@ will allow you to pass a static value into your isolate scope. = on the other hand will allow you to bind an attribute of the isolate scope to a attribute of the parent $scope.

This works using simple watchers. Watchers are created to monitor the values on both $scopes. When one changes, the watcher will update the other.

This can be used for input and output. If the value changes in the isolate $scope it is changed in the parent $scope, and vice versa.

.directive('userProfile', function() {
return {
scope: {
user: '='
},
template: "<h2>{{user.name}}</h2>"
}
})
<div ng-repeat="user in users">
<user-profile user="user"></user-profile>
</div>

One way binding with <

< will let you pass a value in which isn't bound back to the parent. To get data out again you fire an event with &.

.directive('userProfile', function() {
return {
scope: {
user: '<'
},
template: "<h2>{{user.name}}</h2>"
}
})

One way binding is more predictable and performant than two way binding, since you can't accidentally change a value on a parent. This is the default behaviour in Angular 2.

Output with &

We can achieve output from our component by calling functions on the parent $scope. We do this with an &.

.directive('likeButton', function() {
return {
scope: {
onLiked: '&'
},
template: `
<button ng-click='like()'>
Like
</button>
`,
controller: function() {
$scope.like = () => {
$scope.liked = !$scope.liked;
$scope.onLiked({status: $scope.liked})
}
}
}
})

We might use this directive like this:

<like-button on-liked="handleLike(status)"></like-button>

The controller will shell out to the onLiked function on the parent $scope.

When to use isolate scopes

Isolate scopes should be used with care. They are not suitable for every case as they break the simple $scope inheritance tree. It's worth noting that none of the built in Angular directives use isolate scopes.

Use an isolate scope when you have created a reusable component that you might want to use in multiple places, for example, a map, or a login form component.

Don't use an isolate $scope when you just want to change the appearance or behaviour of an existing element, for example, an ng-if or an ng-class.

Isolate scopes with transclusion

Isolate scopes will typically be used with a template to work properly. Only template code will gain access to the isolate scope. Transcluded content (content nested in the directive) will still use the parent scope.

Exercise - Isolate the Flickr app

Give your Flickr app an isolate scope. It should be able to receive a tag from its parent scope.

I want to be able to call it like this:

<input ng-model="search" />
<flickr tag="search"></flickr>

you'll need to use two way = binding to do this.

Harder Exercise - Feed back to the parent template

Make your flickr app receive callback functions from the parent tempalte, so we can, for example, show a spinner. You'll need to use & binding to do this.

I want to be able to call it like this:

<input ng-model="search" />
<flickr
tag="search"
on-loading="showSpinner()"
on-loading-complete="hideSpinner()"></flickr>