You Can Bind To The Same Event Several Times In Angular 2 Beta 17
Yesterday, in my demo on enabling tabbing in textarea elements in Angular 2, I did something that gave me pause - I used [(ngModel)] and (ngModelChange) in the same element. At first, this might not seem interesting. But, when you stop and think about the fact that [(ngModel)] box-of-bananas notation is really just the short-hand syntax for [ngModel] (ngModelChange), then it gets interesting. It means that I was essentially binding to the (ngModelChange) event twice in the same element. After realizing this, I wanted to see if you could arbitrarily bind to the same event several times. Turns out, you totally can.
Run this demo in my JavaScript Demos project on GitHub.
In Angular 1.x, I believe (but am not 100% sure) that templates had to be valid HTML because they were parsed by the browser before Angular started interpreting them. In Angular 2, the parsing has completely changed. Now, Angular 2 has its own internal template parser and it actually converts your template into a syntax tree which it then consumes. This means that an Angular 2 template doesn't necessarily inherit all the same constraints of HTML.
What this means is that our Angular 2 template elements don't just become a "bag of properties"; instead, they are interpreted and translated into Angular 2 magic. A side-effect of this appears to be the fact that we can bind to any given event multiple times in the span of a single element. This is easy to demonstrate:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
You Can Bind To The Same Event Several Times In Angular 2 Beta 17
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
You Can Bind To The Same Event Several Times In Angular 2 Beta 17
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
requirejs(
[ /* Using require() for better readability. */ ],
function run() {
ng.platform.browser.bootstrap( require( "App" ) );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the root App component.
define(
"App",
function registerApp() {
// Configure the App component meta-data.
ng.core
.Component({
selector: "my-app",
directives: [ require( "Widget" ) ],
// In this template, notice that we are binding to the (click)
// event and the (outputEvent) event several times in the span
// of single component element. This works because Angular uses
// its own custom template parser rather than relying on this
// template being valid "HTML".
template:
`
<my-widget
(click)="groupEvents()"
(click)="logValue( 'a' )"
(click)="logValue( 'b' )"
(click)="logValue( 'c' )"
(click)="logValue( 'd' )"
(click)="groupEvents()"
(outputEvent)="logValue( $event + '-a' )"
(outputEvent)="logValue( $event + '-b' )"
(outputEvent)="logValue( $event + '-c' )"
(outputEvent)="logValue( $event + '-d' )">
</my-widget>
`
})
.Class({
constructor: AppController
})
;
return( AppController );
// I control the App component.
function AppController() {
var vm = this;
// Expose the public methods.
vm.groupEvents = groupEvents;
vm.logValue = logValue;
// ---
// PUBLIC METHODS.
// ---
// I wrap all the expected output in a log-group.
function groupEvents() {
console.group( "Click it - click it real good!" );
// We need to execute the ending in a future tick in order to
// give the EventEmitters time to run.
setTimeout( console.groupEnd.bind( console ), 0 );
}
// I love the given value to the console.
function logValue( value ) {
console.log( "Value:", value );
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a component that emits an output event.
define(
"Widget",
function registerWidget() {
// Configure the Widget component meta-data.
ng.core
.Component({
selector: "my-widget",
outputs: [ "outputEvent" ],
host: {
"(click)": "handleClick( $event )"
},
template: "Click Me!"
})
.Class({
constructor: WidgetController
})
;
return( WidgetController );
// I control the Widget component.
function WidgetController() {
var vm = this;
// I expose an output event binding.
vm.outputEvent = new ng.core.EventEmitter( /* isAsync = */ false );
// Expose the public methods.
vm.handleClick = handleClick;
// ---
// PUBLIC METHODS.
// ---
// I handle click events on the host.
function handleClick( event ) {
vm.outputEvent.next( "outputEvent" );
}
}
}
);
</script>
</body>
</html>
As you can see, I'm binding to both DOM (Document Object Model) events and component output events multiple times in a single Widget element. And, when we run the above code and click on the widget, we get the following output:
As you can see, all of the individual event bindings were maintained for both the DOM-based and the component-based events. But, even more than that, notice that the order of the event bindings is meaningful. Meaning, the events were bound in the same order in which they were defined in the template. Very interesting!
I don't have a great use-case for this off the top of my head. Except for the fact that it does (at least in part) allow you to define [(ngModel)] and (ngModelChange) in the same element, which is very useful when responding to the input changes. But, more than anything, this makes me more cognizant of how template parsing works in Angular 2.
Want to use code from this post? Check out the license.
Reader Comments
line 113: "I love the given value to the console" - so good
I don't immediately have a use case for this either, but now that I know you can do this in Angular 2, I'm sure I'll encounter a future scenario where this will be applicable and I'll think back to this post.
@Corey,
Ha ha, that's just a satisfying typo :D