Attribute Directive Selectors And Outputs Cannot Have The Same Name In AngularJS 2 Beta 1
Feb 15, 2016 - UPDATE: This has been fixed as of Beta 6. Now, selectors and outputs CAN HAVE the same value in Angular 2 Beta 6.
I am leaving this post here for historical context.
In AngularJS 1.x, the selector for a directive was completely decoupled from the inputs and outputs of that directive. The selector just acted as a means to associate a given directive definition with a given DOM (Document Object Model) node. After the association was made, inputs and outputs were bound independently. In AngularJS 2 Beta 1, however, the selectors seem to be a bit more coupled to the concept of inputs and outputs. In fact, it seems that attribute directive selectors and outputs can no longer have the same name; at least, not in the way that a developer would intend to define it.
Run this demo in my JavaScript Demos project on GitHub.
In AngularJS 1.x, it would not be uncommon to see an attribute directive respond to an event using the same attribute binding (ie, define a scope method to be invoked in response to an event):
<div bn-foo="vm.handleFoo()">
Here, the bnFoo attribute is both defining the directive selector and providing the event handler for the "foo" event. And, this worked well.
In AngularJS 2 Beta 1, this no longer works - not in the way you'd expect. I know that a big goal for AngularJS 2 was to remove a lot of ambiguity from the template syntax; so, I assume that this feature was removed in an effort to reach that goal. But, if you're coming from an AngularJS 1.x context, this might cause some confusion.
To see this in action, I've created an attribute directive - "giggle" - that attempts to expose an output of the same name, "giggle". In the following code, I'm logging two things:
- Whether or not the GiggleComponent is ever instantiated.
- Whether or not the AppComponent is ever responding to "giggle" events.
In this case, I'm attempting to use "(giggle)" both as an attribute selector and as an output binding:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Attribute Directive Selectors And Outputs Cannot Have The Same Name In AngularJS 2 Beta 1
</title>
</head>
<body>
<h1>
Attribute Directive Selectors And Outputs Cannot Have The Same Name 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.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 provide the root application component.
define(
"AppComponent",
[ "Giggle" ],
function registerAppComponent( Giggle ) {
// Configure the App component definition.
var AppComponent = ng.core
.Component({
selector: "my-app",
directives: [ Giggle ],
// In the following template, were are attempting to define
// an attribute directive using a selector and an output of
// the same name.
// --
// CAUTION: The syntax we are using in the template DOES NOT WORK.
// But, you can get it to work with the following syntax:
// --
// giggle (giggle)="logGiggle( $event )"
// --
// ... where the selector and the output can have the same name;
// but, they need to be duplicated in the calling context.
template:
`
<div (giggle)="logGiggle( $event )">
Go ahead, tickle me!
</div>
`
})
.Class({
constructor: AppController
})
;
return( AppComponent );
// I control the AppComponent component.
function AppController() {
var vm = this;
// Expose the public methods.
vm.logGiggle = logGiggle;
// ---
// PUBLIC METHODS.
// ---
// I log the giggle event.
function logGiggle( message ) {
console.log( "Thou hast giggled:", message );
};
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a directive that emits "giggle" events on an interval.
define(
"Giggle",
function registerGiggle() {
// Configure the Giggle directive definition.
// --
// NOTE: Notice that the attribute selector and the output use the same
// name. This would have been totally fine in AngularJS 1.x; but, appears
// to be invalid in AngularJS 2 Beta 1.
var GiggleDirective = ng.core
.Directive({
selector: "[giggle]",
outputs: [ "giggle" ]
})
.Class({
constructor: GiggleController,
// Define life-cycle events on the prototype so that they will
// be picked-up at link time.
ngOnInit: function noop() {}
})
;
return( GiggleDirective );
// I control the Giggle directive.
function GiggleController() {
var vm = this;
console.log( "The giggle constructor has been invoked." );
// Since "giggle" is an output, we have to set up an event stream
// that the calling context can subscribe to.
vm.giggle = new ng.core.EventEmitter();
// Expose the public methods.
vm.ngOnInit = ngOnInit;
// ---
// PUBLIC METHODS.
// ---
// I get called once, after the directive has been instantiated and
// the inputs have been bound.
function ngOnInit() {
// Set up an interval to start emitting output events.
setInterval(
function deferredLaugh() {
console.log( "- - - - - - - - - - - " );
console.log( "Emitting giggle event." );
vm.giggle.emit( "Te he he" );
},
( 2 * 1000 )
);
}
}
}
);
</script>
</body>
</html>
Unfortunately when we run the above code, we get not console logging. Meaning, the GiggleComponent is never instantiated which, in turn, means that the selector never matched anything in the AppComponent template.
Now, if we change the template syntax to be:
<div giggle="logGiggle( $event )"> ... </div>
... we can see that the GiggleComponent is instantiated and the events are emitted. However, since the AppComponent isn't actually binding to the event - using the (giggle) syntax - the logGiggle() method is never being invoked.
What we can do is change the template syntax to be repetitive:
<div giggle (giggle)="logGiggle( $event )"> ... </div>
... where we're actually doubling-up on the attribute, so to speak. Now, this actually does what we want - it instantiated the GiggleComponent - using the [giggle] attribute selector - and tells the AppComponent to bind to the "giggle" event - using the (giggle) binding. However, while this may work, it is certainly not what we want to write in our code.
I thought perhaps I was just missing something obvious. So, I started looking through the NgUpgrade code that can "upgrade" and AngularJS 1.x component to act like an AngularJS 2.x component. Since AngularJS 1.x allows for directive selectors to also act as "outputs", so to speak, I had hoped that the upgrade code would shed some light on what I was missing. But, the upgrade path (and the upgrade documentation) only seems to support element-selectors, not attribute-selectors. As such, this isn't even a point of compatibility that the upgrade adapter needs to worry about.
With AngularJS 2 Beta 1, the mindset is "components all the way down." But, AngularJS 2 also supports attribute directives. So, it's a little surprising to see that the selectors and the outputs cannot have the same name (in the way that you'd expect). I assume that this has to do with removing syntax ambiguity, especially around the whole [()] two-way binding syntax. But, if you're an AngularJS 1.x developer, just something to keep in the back of your mind when you switch over to AngularJS 2.
Want to use code from this post? Check out the license.
Reader Comments
Great article.
I think it's a good practise not to name directive attributes the same as the directive.
Component directives _can_ have attributes of same name and I think that makes more sense.
Side question: are you planning to use es6 or Typescript?
The syntax is so nice :)
I am aware that part of your learning process is to use es5.
Just curious.
@Lars,
While I think this is a bad idea in most cases, the one use case - probably the primary use case - that I would have used this would be simulate new types of DOM events. Right now, we have (click) and I assume we have (dblclick); but, what if I wanted to bind to a "triple click" event. In AngularJS 1.x, I could have created a new directive (pseudo definition):
tripleClick: { tripleClick: "&" }
... and then been able to arbitrarily attach it to any DOM element:
<div triple-click="vm.handleClickEvent()"> ... </div>
In AngularJS 2 Beta 1, it would be great to have the same kind of binding syntax possible:
<div (tripleClick)="handleClickEvent()"> ... </div>
But, sadly, doesn't work. I am really hoping the AngularJS team just never thought of this particular use-case; and seeing it means that patch-update to make it possible :D
As far as TypeScript vs. ES6. Honestly, I couldn't care less about "type safety". You can add that to the same list with "const" and "let" under the heading, "Solving Problems I Don't Have".
BUT, I will probably use TypeScript in so much as it makes the dependency-injection syntax much easier. So, probably, I'll only ever use the type annotations for DI. Everywhere else, I don't know, it just feels like noise to me. But, of course, it's 100% personal opinion - this is just how I see the code.
Actually, it looks like this is already reported as a bug:
https://github.com/angular/angular/issues/5707
So, hopefully that's a good sign that it will be fixed :D
Please stop calling it AngularJS 2. There is no AngularJS 2. It's called Angular 2.
@John,
I think maybe we're talking about .... 2 .... different things :P
Just kidding - I'll try to get into the right habit for the next post. Started calling it AngularJS 2 and just stopped thinking about it after that.