Dmitry Volkov Dmitry Volkov - 2 months ago 7
TypeScript Question

Angular2 @HostListener won't work in derived component

I have an application with Angular2-based client side.
I have a base class:

abstract class BaseClass {
@HostListener('window:beforeunload') beforeUnloadHandler() {
console.log('bla');
}
}


and two very similar to each other derived classes:

@Component({
selector: 'derived-one',
templateUrl: './templates/app/+derived-one/derived-one.component.html'
})
export class DerivedOne extends BaseClass {
}

@Component({
selector: 'derived-two',
templateUrl: './templates/app/+derived-two/derived-two.component.html'
})
export class DerivedTwo extends BaseClass {
}


The problem is that, for example, in
DerivedOne
beforeUnloadHandler
works fine while in
DerivedTwo
it doesn’t receive a call at all.

I know it’s hard to find the reason why it happens just looking on the information above, but maybe someone might have a suspicion what could cause such strange behavior.

A few more notes:

If I use the following :

abstract class BaseClass
constructor(){
window.onbeforeunload = function(){
console.log('bla');
}
}
}


everything works fine, but I still would like to find an Angular2-based solution;

If I write

abstract class BaseClass {
beforeUnloadHandler() {
console.log('bla');
}
}


and in
derived-two.component.html


<div (window.beforeunload)="beforeUnloadHandler()"></div>


everything works fine too, but it looks like an ugly hack;

Again, if I write

abstract class BaseClass {
beforeUnloadHandler() {
console.log('bla');
}
}


and

@Component({
selector: 'derived-two',
host: {'window:beforeunload': 'beforeUnloadHandler' }
templateUrl: './templates/app/+derived-two/derived-two.component.html'
})
export class DerivedTwo extends BaseClass {
}


it won’t work.

Finally, if I use
@HostListener
in
DerivedTwo
and in
DerivedOne
, it works, but I would like to avoid using duplicate code.

Hopefully, the information above would be enough to work with (at least to have some guesses).

Answer

1) If you have a class:

abstract class BaseClass {
  @HostListener('window:beforeunload') beforeUnloadHander() {
    console.log('bla');
  }
}

then it will work

Plunker Example (put whitespace somewhere in editor and watch console)

but be careful since Angular2 doesn't support the full inheritance - Issue with binding and @ViewChild

But it still unclear why the solution with @HostListener didn't work in first place

Specifically if you have a property decorator on your derived component it won't work. For example let's say we have the following code:

abstract class BaseClass {
  @HostListener('window:beforeunload') beforeUnloadHander() {
    console.log(`bla-bla from${this.constructor.name}`);
  } 
} 

@Component({
    selector:  'derived-one',
    template:  '<h2>derived-one</h2>'
})
export class DerivedOne extends BaseClass {
   @Input() test;
}

Plunker

It will be transformed to javascript like:

var core_1 = require('@angular/core');
var BaseClass = (function () {
    function BaseClass() {
    }
    BaseClass.prototype.beforeUnloadHander = function () {
        console.log("bla-bla from" + this.constructor.name);
    };
    __decorate([
        core_1.HostListener('window:beforeunload'), 
        __metadata('design:type', Function), 
        __metadata('design:paramtypes', []), 
        __metadata('design:returntype', void 0)
    ], BaseClass.prototype, "beforeUnloadHander", null);
    return BaseClass;
}());
var DerivedOne = (function (_super) {
    __extends(DerivedOne, _super);
    function DerivedOne() {
        _super.apply(this, arguments);
    }
    __decorate([
        core_1.Input(), 
        __metadata('design:type', Object)
    ], DerivedOne.prototype, "test", void 0);
    DerivedOne = __decorate([
        core_1.Component({
            selector: 'derived-one',
            template: '<h2>derived-one</h2>'
        }), 
        __metadata('design:paramtypes', [])
    ], DerivedOne);
    return DerivedOne;
}(BaseClass));

We are interested in the following lines:

 __decorate([
    core_1.HostListener('window:beforeunload'), 
      __metadata('design:type', Function), 
      __metadata('design:paramtypes', []), 
      __metadata('design:returntype', void 0)
 ], BaseClass.prototype, "beforeUnloadHander", null);

 ... 
 __decorate([
   core_1.Input(), 
   __metadata('design:type', Object)
 ], DerivedOne.prototype, "test", void 0);

HostListener and Input are property decorators (propMetadata key). This way will define two metadata entries - on BaseClass and on DerivedOne enter image description here enter image description here

Finally when angular2 will extract metadata from DerivedOne class it will only use its own metadata:

enter image description here

To get all the metadata you can write custom decorator like:

function InheritPropMetadata() {
  return (target: Function) => {
    const targetProps = Reflect.getMetadata('propMetadata', target);

    const parentTarget = Object.getPrototypeOf(target.prototype).constructor;
    const parentProps = Reflect.getMetadata('propMetadata', parentTarget);

    const mergedProps = Object.assign(targetProps, parentProps);

    Reflect.defineMetadata('propMetadata', mergedProps, target);
  };
};

@InheritPropMetadata()
export class DerivedOne extends BaseClass {

Here's a working demo

2) If you done as follows:

abstract class BaseClass
  constructor(){
    window.onbeforeunload = function(){
      console.log('bla');
    };
  }
}

then it will be invoked only one time because you're overriding window.onbeforeunload handler everytime You should use the following instead:

abstract class BaseClass {
 constructor(){
    window.addEventListener('beforeunload', () =>{
      console.log(`bla-bla from${this.constructor.name}`);
    })
  }
}  

Plunker Example

3) Finally if you have base class as shown below:

abstract class BaseClass {
  beforeUnloadHander() {
     console.log(`bla-bla from${this.constructor.name}`);
  }
}

then you have to use the correct syntax (you're missing brackets) in decorator property:

host: {'(window:beforeunload)': 'beforeUnloadHander()' }

Plunker Example

Hope it helps you!