Angular JS $onChanges component hook use case: switching between Youtube and Vimeo players based on URL

Radek Anuszewski
Frontend Weekly
Published in
3 min readDec 11, 2016

At first glance, component’s $onChanges hook introduced in Angular 1.5.3 looks a little bit weird. But it shows its power when we have to calculate internal state of component, based on state given from parent— then $onChanges’s changes object appears to be indispensable, helping to dispose of $scope object, in most cases unwanted in modern AngularJS development.

Displaying Youtube or Vimeo videos on page

UPDATE 2017–02–12: few days ago I wrote a post: AngularJS: $onChanges component hook as solution for not ready bindings which uses very similar way to solve problems with asynchronous loading.

Let’s say we want to build a component, which displays video on our page. To be reusable in different parts of application, it should be as stateless (or dumb :) watch Shai Reznik — Smart and Dumb Components) as possible. Todd Motto is his article about stateless and stateful components gave an example of characteristics of stateless component:

Do not request/fetch data

Are passed data via property binding

Emit data via event callbacks

Renders further stateless (or even stateful) components

Can contain local UI state

Are a small piece of a bigger picture

So it will be perfectly OK to have local state, deciding on whether Youtube or Vimeo player should be displayed, depending on provided URL.

Prototype of component

So, usage of component may look like this:

<ra-video url=”$ctrl.videoUrl”></ra-video>

And here’s component itself, without any logic at this moment:

(function(){

function VideoController () {
var $ctrl = this;
}

function raVideoComponent() {
return {
bindings: {
url: '<',
},
controller: VideoController,
templateUrl: 'ra-video.component.html',
}
}

angular.module('ncAdminApp')
.component('raVideo', raVideoComponent());

}());

In component’s template, we see 2 boolean values responsible for displaying proper video player:

<youtube-video
ng-if="$ctrl.isYoutube"
video-url="$ctrl.url"
></youtube-video>
<vimeo-video
ng-if="$ctrl.isVimeo"
player-id="vimeoVideo"
video-url="$ctrl.url"
>
</vimeo-video>

So we need to add them to component’s controller in next step. Following libraries are used for displaying players: angular-youtube-embed by bradly for Youtube and angular-vimeo-embed by vincenzomerolla for Vimeo.

Old way of detecting changes, with $scope.$watch()

Before Angular 1.5.3, first attempt to make component working may include $onInit() hook, introduced in 1.5.0:

$ctrl.$onInit = function () { 
// helper functions, e.g. may use regular expressions
$ctrl.isYoutube = _isYoutube($ctrl.url);
$ctrl.isVimeo = _isVimeo($ctrl.url);
}

Although $onInit() hook is great and has its use cases (like simplifying unit tests, for example), it won’t help us with our problem. It runs only once, and there is no detection for any further changes. The solution is to inject $scope and create a watcher:

$ctrl.$onInit = function()
var unsubscribeFromWatchUrl = $scope.$watch(function(){return $ctrl.url},
function(newValue, oldValue) {
$ctrl.isYoutube = _isYoutube(newValue);
$ctrl.isVimeo = _isVimeo(newValue);
}
)
}

And we have to remember to unsubscribe on component’s $scope destroy:

$scope.$on(“$destroy”, function() { 
unsubscribeFromWatchUrl();
})

(By the way, beginning from Angular 1.5.3 $onDestroy() hook should be used for unsubscribing) Important note: watcher is set inside $onInit(), because from Angular 1.6 bindings are not available before $onInit().

New, great way of detecting changes with $onChanges() hook

$onChanges() is modern solution for our problem. It’s close to ngOnChanges hook from Angular 2, it takes changes object as parameter, where keys are bindings names that changes. In our case, if url key is present, we can take its currentValue:

$ctrl.onChanges = function (changes) { 
if (changes.url) {
$ctrl.isYoutube = _isYoutube(changes.url.currentValue);
$ctrl.isVimeo = _isVimeo(changes.url.currentValue);
}
}

And because $onChanges() is called before $onInit() we don’t have to duplicate logic in $onInit() hook. Important note: it is intentional to do not check if currentValue is present (like if (changes.url && changes.url.currentValue)), because undefined/null may be a valid value — for example, user deleted video with delete button in parent component and placeholder is shown.

Conclusion

$onChanges hook, although it seemingly may look useless, is yet another important way of getting rid of $scope and being closer to Angular 2. It also allows us to easily decompose component input — without it, in discussed here case, we would need to add boolean for every player type. With $onChanges, we can set it internally without messing up parent component.

--

--

Radek Anuszewski
Frontend Weekly

Software developer, frontend developer in AltConnect.pl, mostly playing with ReactJS, AngularJS and, recently, BackboneJS / MarionetteJS.