Selectors And Outputs Can Have The Same Name In Angular 2 Beta 6
A few weeks ago, I demonstrated that selectors and outputs could not have the same name in Angular 2 Beta 1. Today, I am very excited to demonstrate that this has been fixed in Angular 2 Beta 6. Now, a directive selector can also act as an output event binding. This makes it much easier to create behavioral directives that do nothing but provide element-level behaviors.
Run this demo in my JavaScript Demos project on GitHub.
To demonstrate this newly fixed functionality, I'm going to create a "clickOutside" directive in which the value, "clickOutside," acts as both the attribute directive selector and the output event name:
(clickOutside)="doSomething( $event )"
The goal of this directive is to invoke the given callback when the user clicks outside of the target element.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Selectors And Outputs Can Have The Same Name In Angular 2 Beta 6
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Selectors And Outputs Can Have The Same Name In Angular 2 Beta 6
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/6/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 App component.
define(
"AppComponent",
function registerAppComponent() {
var ClickOutside = require( "ClickOutside" );
// Configure the App component definition.
ng.core
.Component({
selector: "my-app",
// Here, we are providing the [clickOutside] directive which also
// emits an event of the same name (clickOutside) (for when the
// user clicks outside of the bound element).
directives: [ ClickOutside ],
template:
`
<p
(click)="handleClick( $event.target.tagName )"
(clickOutside)="handleClickOutside( $event.target.tagName )">
Click, or click not, there is no mouse.
</p>
`
})
.Class({
constructor: AppController
})
;
return( AppController );
// I control the App component.
function AppController() {
var vm = this;
// Expose the public methods.
vm.handleClick = handleClick;
vm.handleClickOutside = handleClickOutside;
// ---
// PUBLIC METHODS.
// ---
// I handle the click internally to the bound target.
function handleClick( tagName ) {
console.log( "Ouch!", tagName );
}
// I handle the click externally to the bound target.
function handleClickOutside( tagName ) {
console.log( "Click outside!", tagName );
}
}
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a directive whose selector [clickOutside] and output (clickOutside)
// are the same value.
// --
// CAUTION: This only started working in Beta 6.
define(
"ClickOutside",
function registerClickOutside() {
// Configure the ClickOutside directive definition.
// --
// Notice that the value of the attribute selector and the output event
// are the same - "clickOutside".
ng.core
.Directive({
selector: "[clickOutside]",
outputs: [ "clickOutside" ],
host: {
"(click)": "trackEvent( $event )",
"(document: click)": "compareEvent( $event )"
}
})
.Class({
constructor: ClickOutsideController
})
;
return( ClickOutsideController );
// I control the ClickOutside directive.
function ClickOutsideController() {
var vm = this;
// Setup the output event stream.
vm.clickOutside = new ng.core.EventEmitter();
// I keep track of the last internal click event so that we can
// compare target-local events to global events.
var localEvent = null;
// Expose the public methods.
vm.compareEvent = compareEvent;
vm.trackEvent = trackEvent;
// ---
// PUBLIC METHODS.
// ---
// I track and compare the click event at the document root.
function compareEvent( event ) {
// If the event at the document root is the same reference as the
// event at the target, it means that the event originated from
// within the target and bubbled all the way to the root. As such,
// if the event at the document root does NOT MATCH the last known
// event at the target, the event must have originated from
// outside of the target.
if ( event !== localEvent ) {
vm.clickOutside.emit( event );
}
localEvent = null;
}
// I track the click event on the bound target.
function trackEvent( event ) {
// When the user clicks inside the bound target, we need to start
// tracking the event as it bubbles up the DOM tree. This way,
// when a click event hits the document root, we can determine if
// the event originated from within the target.
localEvent = event;
}
}
}
);
</script>
</body>
</html>
As you can see, the "clickOutside" attribute is acting as both the attribute selector, "[clickOutside]", and the host event binding, "(clickOutside)". And, when we run this page and click around the document, we get the following output:
As you can see, it worked perfectly.
The point here is not the logic of the ClickOutside directive, but rather to showcase the fact that the selector and the output can now use the same name as of Angular 2 Beta 6. Awesome stuff!
Want to use code from this post? Check out the license.
Reader Comments
love the articles.. but really having hard time translating to TypeScript, can I but a TypeScript book? it's on me :)
@Sean,
Ha ha, to be fair, though even with TypeScript, I am not sure how much would change. If I wanted to keep things all on the same page, I would still need to use RequireJS / AlmondJS and the define() function in lieu of components in separate files. The meta-data decorators would become a bit more simple. But, really, not all that much would change. Because, keep in mind, I'm not using the prototype for my methods most of the time. Meaning that this:
class Foo {
constructor() { .. }
doThis() { ... }
doThat() { ... }
}
... is NOT the "ES6" equivalent of:
function Foo() {
this.doThis = doThis;
this.doThat = doThat;
}
The former is using the class Prototype to store the methods. My version is creating a new copy of the methods for each instance. Which allows things like private variables and private methods.
All to say, it's not just a different syntax - it's a different pattern of object creation.
That said, I really do think that the ES5 is helping me understand things better since I *have to be thinking* about these very differences.
Great approach, thanks for sharing!
@Patrick,
My pleasure!