Reputation: 1337
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).
Upvotes: 7
Views: 5857
Reputation: 214175
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;
}
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
Finally when angular2 will extract metadata from DerivedOne
class it will only use its own metadata:
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}`);
})
}
}
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()' }
Hope it helps you!
Upvotes: 12