Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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).

like image 744
Dmitry Volkov Avatar asked Sep 21 '16 10:09

Dmitry Volkov


1 Answers

Update 2.3.0

You can now take advantage of object inheritance for components.

More details you can see in this commit https://github.com/angular/angular/commit/f5c8e0989d85bc064f689fc3595207dfb29413f4

Old Version

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!

like image 136
yurzui Avatar answered Nov 02 '22 23:11

yurzui