diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts
index 45992b9c5c..7b63931092 100644
--- a/packages/core/src/core_render3_private_export.ts
+++ b/packages/core/src/core_render3_private_export.ts
@@ -60,6 +60,7 @@ export {
e as ɵe,
p as ɵp,
pD as ɵpD,
+ a as ɵa,
s as ɵs,
t as ɵt,
v as ɵv,
diff --git a/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts b/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts
index de141cd4ac..accd1e85f9 100644
--- a/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts
+++ b/packages/core/test/render3/compiler_canonical/compiler_canonical_spec.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Component, ContentChild, ContentChildren, Directive, HostBinding, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../src/core';
+import {Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../src/core';
import * as $r3$ from '../../src/core_render3_private_export';
import {renderComponent, toHtml} from '../render_util';
@@ -174,7 +174,102 @@ describe('compiler specification', () => {
}
expect(renderComp(MyApp)).toEqual(`
`);
+ });
+ it('should support host listeners', () => {
+ type $MyApp$ = MyApp;
+
+ @Directive({selector: '[hostlistenerDir]'})
+ class HostListenerDir {
+ @HostListener('click')
+ onClick() {}
+
+ // NORMATIVE
+ static ngDirectiveDef = $r3$.ɵdefineDirective({
+ type: HostListenerDir,
+ factory: function HostListenerDir_Factory() {
+ const $dir$ = new HostListenerDir();
+ $r3$.ɵL('click', function HostListenerDir_click_Handler(event) { $dir$.onClick(); });
+ return $dir$;
+ },
+ });
+ // /NORMATIVE
+ }
+
+ const $e0_attrs$ = ['hostListenerDir', ''];
+ const $e0_dirs$ = [HostListenerDir];
+
+ @Component({
+ selector: 'my-app',
+ template: `
+
+ `
+ })
+ class MyApp {
+ static ngComponentDef = $r3$.ɵdefineComponent({
+ type: MyApp,
+ tag: 'my-app',
+ factory: function MyApp_Factory() { return new MyApp(); },
+ template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) {
+ if (cm) {
+ $r3$.ɵE(0, 'button', $e0_attrs$, $e0_dirs$);
+ $r3$.ɵT(2, 'Click');
+ $r3$.ɵe();
+ }
+ HostListenerDir.ngDirectiveDef.h(1, 0);
+ $r3$.ɵr(1, 0);
+ }
+ });
+ }
+
+ expect(renderComp(MyApp)).toEqual(``);
+ });
+
+ it('should support bindings of host attributes', () => {
+ type $MyApp$ = MyApp;
+
+ @Directive({selector: '[hostBindingDir]'})
+ class HostBindingDir {
+ @HostBinding('attr.aria-label') label = 'some label';
+
+ // NORMATIVE
+ static ngDirectiveDef = $r3$.ɵdefineDirective({
+ type: HostBindingDir,
+ factory: function HostBindingDir_Factory() { return new HostBindingDir(); },
+ hostBindings: function HostBindingDir_HostBindings(
+ dirIndex: $number$, elIndex: $number$) {
+ $r3$.ɵa(elIndex, 'aria-label', $r3$.ɵb($r3$.ɵm(dirIndex).label));
+ }
+ });
+ // /NORMATIVE
+ }
+
+ const $e0_attrs$ = ['hostBindingDir', ''];
+ const $e0_dirs$ = [HostBindingDir];
+
+ @Component({
+ selector: 'my-app',
+ template: `
+
+ `
+ })
+ class MyApp {
+ static ngComponentDef = $r3$.ɵdefineComponent({
+ type: MyApp,
+ tag: 'my-app',
+ factory: function MyApp_Factory() { return new MyApp(); },
+ template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) {
+ if (cm) {
+ $r3$.ɵE(0, 'div', $e0_attrs$, $e0_dirs$);
+ $r3$.ɵe();
+ }
+ HostBindingDir.ngDirectiveDef.h(1, 0);
+ $r3$.ɵr(1, 0);
+ }
+ });
+ }
+
+ expect(renderComp(MyApp)).toEqual(``);
});
xit('should support structural directives', () => {
diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts
index 449c55f9a1..c7ba705a0b 100644
--- a/packages/core/test/render3/integration_spec.ts
+++ b/packages/core/test/render3/integration_spec.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {defineComponent} from '../../src/render3/index';
+import {defineComponent, defineDirective} from '../../src/render3/index';
import {NO_CHANGE, bind, componentRefresh, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, memory, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
import {containerEl, renderToHtml} from './render_util';
@@ -664,6 +664,41 @@ describe('render3 integration test', () => {
expect(renderToHtml(Template, ctx))
.toEqual('');
});
+
+ it('should support host attribute bindings', () => {
+ let hostBindingDir: HostBindingDir;
+
+ class HostBindingDir {
+ /* @HostBinding('attr.aria-label') */
+ label = 'some label';
+
+ static ngDirectiveDef = defineDirective({
+ type: HostBindingDir,
+ factory: function HostBindingDir_Factory() {
+ return hostBindingDir = new HostBindingDir();
+ },
+ hostBindings: function HostBindingDir_HostBindings(dirIndex: number, elIndex: number) {
+ elementAttribute(elIndex, 'aria-label', bind(memory(dirIndex).label));
+ }
+ });
+ }
+
+ function Template(ctx: any, cm: boolean) {
+ if (cm) {
+ elementStart(0, 'div', ['hostBindingDir', ''], [HostBindingDir]);
+ elementEnd();
+ }
+ HostBindingDir.ngDirectiveDef.h(1, 0);
+ componentRefresh(1, 0);
+ }
+
+ expect(renderToHtml(Template, {}))
+ .toEqual(``);
+
+ hostBindingDir !.label = 'other label';
+ expect(renderToHtml(Template, {}))
+ .toEqual(``);
+ });
});
describe('elementStyle', () => {
diff --git a/packages/core/test/render3/listeners_spec.ts b/packages/core/test/render3/listeners_spec.ts
index ea58a8eae6..613b2c1cc4 100644
--- a/packages/core/test/render3/listeners_spec.ts
+++ b/packages/core/test/render3/listeners_spec.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {defineComponent} from '../../src/render3/index';
+import {defineComponent, defineDirective} from '../../src/render3/index';
import {componentRefresh, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, listener, text} from '../../src/render3/instructions';
import {containerEl, renderComponent, renderToHtml} from './render_util';
@@ -29,7 +29,7 @@ describe('event listeners', () => {
if (cm) {
elementStart(0, 'button');
{
- listener('click', ctx.onClick.bind(ctx));
+ listener('click', function() { ctx.onClick(); });
text(1, 'Click me');
}
elementEnd();
@@ -55,6 +55,40 @@ describe('event listeners', () => {
expect(comp.counter).toEqual(2);
});
+ it('should call function chain on event emit', () => {
+ /** */
+ function Template(ctx: any, cm: boolean) {
+ if (cm) {
+ elementStart(0, 'button');
+ {
+ listener('click', function() {
+ ctx.onClick();
+ ctx.onClick2();
+ });
+ text(1, 'Click me');
+ }
+ elementEnd();
+ }
+ }
+
+ const ctx = {
+ counter: 0,
+ counter2: 0,
+ onClick: function() { this.counter++; },
+ onClick2: function() { this.counter2++; }
+ };
+ renderToHtml(Template, ctx);
+ const button = containerEl.querySelector('button') !;
+
+ button.click();
+ expect(ctx.counter).toBe(1);
+ expect(ctx.counter2).toBe(1);
+
+ button.click();
+ expect(ctx.counter).toBe(2);
+ expect(ctx.counter2).toBe(2);
+ });
+
it('should evaluate expression on event emit', () => {
/** */
@@ -62,7 +96,7 @@ describe('event listeners', () => {
if (cm) {
elementStart(0, 'button');
{
- listener('click', () => ctx.showing = !ctx.showing);
+ listener('click', function() { ctx.showing = !ctx.showing; });
text(1, 'Click me');
}
elementEnd();
@@ -97,7 +131,7 @@ describe('event listeners', () => {
if (embeddedViewStart(1)) {
elementStart(0, 'button');
{
- listener('click', ctx.onClick.bind(ctx));
+ listener('click', function() { ctx.onClick(); });
text(1, 'Click me');
}
elementEnd();
@@ -125,6 +159,42 @@ describe('event listeners', () => {
expect(comp.counter).toEqual(2);
});
+ it('should support host listeners', () => {
+ let events: string[] = [];
+
+ class HostListenerDir {
+ /* @HostListener('click') */
+ onClick() { events.push('click!'); }
+
+ static ngDirectiveDef = defineDirective({
+ type: HostListenerDir,
+ factory: function HostListenerDir_Factory() {
+ const $dir$ = new HostListenerDir();
+ listener('click', function() { $dir$.onClick(); });
+ return $dir$;
+ },
+ });
+ }
+
+ function Template(ctx: any, cm: boolean) {
+ if (cm) {
+ elementStart(0, 'button', ['hostListenerDir', ''], [HostListenerDir]);
+ text(2, 'Click');
+ elementEnd();
+ }
+ HostListenerDir.ngDirectiveDef.h(1, 0);
+ componentRefresh(1, 0);
+ }
+
+ renderToHtml(Template, {});
+ const button = containerEl.querySelector('button') !;
+ button.click();
+ expect(events).toEqual(['click!']);
+
+ button.click();
+ expect(events).toEqual(['click!', 'click!']);
+ });
+
it('should destroy listeners in nested views', () => {
/**
@@ -152,7 +222,7 @@ describe('event listeners', () => {
if (embeddedViewStart(0)) {
elementStart(0, 'button');
{
- listener('click', ctx.onClick.bind(ctx));
+ listener('click', function() { ctx.onClick(); });
text(1, 'Click');
}
elementEnd();
@@ -267,7 +337,7 @@ describe('event listeners', () => {
if (embeddedViewStart(0)) {
elementStart(0, 'button');
{
- listener('click', () => ctx.counter1++);
+ listener('click', function() { ctx.counter1++; });
text(1, 'Click');
}
elementEnd();
@@ -282,7 +352,7 @@ describe('event listeners', () => {
if (embeddedViewStart(0)) {
elementStart(0, 'button');
{
- listener('click', () => ctx.counter2++);
+ listener('click', function() { ctx.counter2++; });
text(1, 'Click');
}
elementEnd();
diff --git a/packages/core/test/render3/outputs_spec.ts b/packages/core/test/render3/outputs_spec.ts
index f1032ddb1c..12fffd7168 100644
--- a/packages/core/test/render3/outputs_spec.ts
+++ b/packages/core/test/render3/outputs_spec.ts
@@ -46,7 +46,9 @@ describe('outputs', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, ButtonToggle);
- { listener('change', ctx.onChange.bind(ctx)); }
+ {
+ listener('change', function() { ctx.onChange(); });
+ }
elementEnd();
}
ButtonToggle.ngComponentDef.h(1, 0);
@@ -70,8 +72,8 @@ describe('outputs', () => {
if (cm) {
elementStart(0, ButtonToggle);
{
- listener('change', ctx.onChange.bind(ctx));
- listener('reset', ctx.onReset.bind(ctx));
+ listener('change', function() { ctx.onChange(); });
+ listener('reset', function() { ctx.onReset(); });
}
elementEnd();
}
@@ -96,7 +98,9 @@ describe('outputs', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, ButtonToggle);
- { listener('change', () => ctx.counter++); }
+ {
+ listener('change', function() { ctx.counter++; });
+ }
elementEnd();
}
ButtonToggle.ngComponentDef.h(1, 0);
@@ -130,7 +134,9 @@ describe('outputs', () => {
if (ctx.condition) {
if (embeddedViewStart(0)) {
elementStart(0, ButtonToggle);
- { listener('change', ctx.onChange.bind(ctx)); }
+ {
+ listener('change', function() { ctx.onChange(); });
+ }
elementEnd();
}
ButtonToggle.ngComponentDef.h(1, 0);
@@ -180,7 +186,9 @@ describe('outputs', () => {
if (ctx.condition2) {
if (embeddedViewStart(0)) {
elementStart(0, ButtonToggle);
- { listener('change', ctx.onChange.bind(ctx)); }
+ {
+ listener('change', function() { ctx.onChange(); });
+ }
elementEnd();
}
ButtonToggle.ngComponentDef.h(1, 0);
@@ -241,12 +249,14 @@ describe('outputs', () => {
if (embeddedViewStart(0)) {
elementStart(0, 'button');
{
- listener('click', ctx.onClick.bind(ctx));
+ listener('click', function() { ctx.onClick(); });
text(1, 'Click me');
}
elementEnd();
elementStart(2, ButtonToggle);
- { listener('change', ctx.onChange.bind(ctx)); }
+ {
+ listener('change', function() { ctx.onChange(); });
+ }
elementEnd();
elementStart(4, DestroyComp);
elementEnd();
@@ -300,7 +310,9 @@ describe('outputs', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, 'button', null, [MyButton]);
- { listener('click', ctx.onClick.bind(ctx)); }
+ {
+ listener('click', function() { ctx.onClick(); });
+ }
elementEnd();
}
}
@@ -323,7 +335,9 @@ describe('outputs', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, ButtonToggle, null, [OtherDir]);
- { listener('change', ctx.onChange.bind(ctx)); }
+ {
+ listener('change', function() { ctx.onChange(); });
+ }
elementEnd();
}
ButtonToggle.ngComponentDef.h(1, 0);
@@ -354,7 +368,9 @@ describe('outputs', () => {
function Template(ctx: any, cm: boolean) {
if (cm) {
elementStart(0, ButtonToggle, null, [OtherDir]);
- { listener('change', ctx.onChange.bind(ctx)); }
+ {
+ listener('change', function() { ctx.onChange(); });
+ }
elementEnd();
}
elementProperty(0, 'change', bind(ctx.change));
@@ -387,7 +403,7 @@ describe('outputs', () => {
if (cm) {
elementStart(0, 'button');
{
- listener('click', ctx.onClick.bind(ctx));
+ listener('click', function() { ctx.onClick(); });
text(1, 'Click me');
}
elementEnd();
@@ -398,7 +414,9 @@ describe('outputs', () => {
if (ctx.condition) {
if (embeddedViewStart(0)) {
elementStart(0, ButtonToggle);
- { listener('change', ctx.onChange.bind(ctx)); }
+ {
+ listener('change', function() { ctx.onChange(); });
+ }
elementEnd();
}
ButtonToggle.ngComponentDef.h(1, 0);
@@ -407,7 +425,9 @@ describe('outputs', () => {
} else {
if (embeddedViewStart(1)) {
elementStart(0, 'div', null, [OtherDir]);
- { listener('change', ctx.onChange.bind(ctx)); }
+ {
+ listener('change', function() { ctx.onChange(); });
+ }
elementEnd();
}
embeddedViewEnd();