Inputs Are Not Pre-Bound To Component Controllers In AngularJS 2 Beta 1
As I've been digging into AngularJS 2 Beta 1, I noticed a "breaking change" from AngularJS 1.x. Now, obviously, AngularJS 2 is a totally different platform; so, I use the term "breaking change" in much more of a philosophical sense rather than a technical one. But, if you're coming from an AngularJS 1.x background, it might be a behavioral change worth noting. In AngularJS 2 Beta 1, inputs are not being pre-bound to component controllers like they were in AngularJS 1.x - they are only available after the first ngOnChanges life-cycle event.
Run this demo in my JavaScript Demos project on GitHub.
Seeing this in action is easy. All we have to do is setup a conditionally-rendered component that accepts an input. Then, we have to log the availability of that input to the console. In the following code, I'm passing the "message" property from the AppComponent into the WidgetComponent.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Inputs Are Not Pre-Bound To Component Controllers In AngularJS 2 Beta 1
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Inputs Are Not Pre-Bound To Component Controllers In AngularJS 2 Beta 1
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/angular2-all.umd.min.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/1/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
// --
// NOTE: Not all components have to be required here since they will be
// implicitly required by other components.
requirejs(
[ "AppComponent" ],
function run( AppComponent ) {
ng.platform.browser.bootstrap( AppComponent );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the root of the application.
define(
"AppComponent",
[ "Widget" ],
function registerAppComponent( Widget ) {
// Configure the App component definition.
// --
// NOTE: I am choosing to use the Template element so as to remove all
// noise from the Widget element, leaving it with nothing but message.
var AppComponent = ng.core
.Component({
selector: "my-app",
directives: [ Widget ],
template:
`
<p>
<a (click)="toggleComponent()">Toggle Component</a>
</p>
<template [ngIf]="isShowingComponent">
<widget [message]="messageForInput"></widget>
</template>
`
})
.Class({
constructor: AppController
})
;
return( AppComponent );
// I control the App component.
function AppController() {
var vm = this;
// I determine if the widget component is being rendered.
vm.isShowingComponent = false;
// I am the property being passed into the widget.
vm.messageForInput = "Woot, passed it in like a Boss!";
// Expose the public methods.
vm.toggleComponent = toggleComponent;
// ---
// PUBLIC METHODS.
// ---
// I toggle the existence of the widget component.
function toggleComponent() {
console.log( "- - - - - - - - - - - - - -" );
vm.isShowingComponent = ! vm.isShowingComponent
};
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a widget for testing the integration of inputs into component
// life-cycle events.
define(
"Widget",
function registerWidget() {
// Configure the widget component definition. Notice that we are exposing
// the "message" as a settable property.
var WidgetComponent = ng.core
.Component({
selector: "widget",
inputs: [ "message" ],
template:
`
<p>
{{ message }}
</p>
`
})
.Class({
constructor: WidgetController,
// Defining the life-cycle methods on the prototype so that
// AngularJS will pick it up at invocation time.
ngOnChanges: function noop() {},
ngOnInit: function noop() {}
})
;
return( WidgetController );
// I control the widget component.
function WidgetController() {
var vm = this;
console.log( "constructor[ message ]:", vm.message );
// Expose the public methods.
vm.ngOnChanges = ngOnChanges;
vm.ngOnInit = ngOnInit;
// ---
// PUBLIC METHODS.
// ---
// The onChanges event is the first event triggered in the component
// life-cycle.
function ngOnChanges() {
console.log( "ngOnChanges[ message ]:", vm.message );
}
// The onInit event is the second event triggered in the component
// life-cycle.
function ngOnInit() {
console.log( "ngOnInit[ message ]:", vm.message );
}
}
}
);
</script>
</body>
</html>
As you can see, I'm logging the value of the message input in the:
- Widget component constructor.
- The ngOnChanges life-cycle event handler.
- The ngOnInit life-cycle event handler.
And, when we run the above code, we get the following output:
As you can see, the "message" input is "undefined" in the component controller's constructor method. It's not until the ngOnChanges life-cycle event fires that the message property is bound to the controller.
In the AngularJS 2 quick-start guide and the dev-guide, there are several references to the way in which the constructor should be used. Specifically, the guide mentions that constructors shouldn't be used to do any heavy lifting. While I had assumed that this statement was one of personal style, I can now see that there are, in fact, actual technical reasons as to why the construtor is limited in what it can do.
Earlier, I referred to this as a "breaking change." As such, I wanted to quickly compare the AngularJS 2 Beta 1 demo to an equivalent Angular JS 1.x demo:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Inputs Are Pre-Bound To Controllers In AngularJS 1.x
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController as vm">
<h1>
Inputs Are Pre-Bound To Controllers In AngularJS 1.x
</h1>
<p>
<a ng-click="vm.toggleComponent()">Toggle Component</a>
</p>
<!--
Here, we are setting the property expression for message.
This is the equivalent of:
[message]="messageForInput"
... in AngularJS 2 Beta 1.
-->
<widget
ng-if="vm.isShowingComponent"
message="vm.messageForInput">
</widget>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.7.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
var vm = this;
// I determine if the widget component is being rendered.
vm.isShowingComponent = false;
// I am the property being passed into the widget.
vm.messageForInput = "Woot, passed it in like a Boss!";
// Expose the public methods.
vm.toggleComponent = toggleComponent;
// ---
// PUBLIC METHODS.
// ---
// I toggle the existence of the widget component.
function toggleComponent() {
console.log( "- - - - - - - - - - - - - -" );
vm.isShowingComponent = ! vm.isShowingComponent
};
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a component for testing input availability.
app.directive(
"widget",
function widgetDirective() {
// Return the directive configuration object.
// --
// NOTE: I am binding the inputs directly to the controller. Normally,
// in AngularJS 1.x, I would bind to the scope (not the controller) so
// that I could differentiate more clearly between "props" and the
// local view-model values. But, I am using bindToController here in
// order to create a tighter similarity to the AngularJS 2 Beta 1 code.
return({
bindToController: true,
controller: WidgetController,
controllerAs: "vm",
restrict: "E",
scope: {
message: "="
},
template:
`
<p>
{{ vm.message }}
</p>
`
});
// I control the Widget template.
function WidgetController( $scope ) {
console.log( "constructor[ message ]:", this.message );
// Start watching for initialization or changes in the message input.
// --
// NOTE: If the message === oldValue, it's the "setup phase".
$scope.$watch(
"vm.message",
function handleMessage( message, oldValue ) {
console.log( "$watch[ message ]:", message );
}
);
}
}
);
</script>
</body>
</html>
As you can see, this demo is basically the same. Only, we're using the scope.$watch() handler instead of the ngOnChanges life-cycle event binding. And, when we run this code, we get the following output:
As you can see, in AngularJS 1.x, the inputs are pre-bound to the component controller and are therefore available within the constructor.
If you're new to AngularJS, this change is totally inconsequential. But, if you are coming from an AngularJS 1.x background, this change in the pre-binding of inputs may trip you up. If nothing else, it's just good to have this in the back of your mind when thinking about when and how data is made available to your AngularJS 2 Beta 1 components.
Want to use code from this post? Check out the license.
Reader Comments