Skip to content

Component Class Inheritance & Decorator bugs #203

Description

@mtraynham

As a preamble on how I came across these bugs, I have a form that uses a variety of components, from string inputs to selects, etc. Some of the components may be more custom than others, but they all share a set of input/output bindings and may share some @Host components.

I tried this with both core-js and reflect-metadata with the same results.

The two issues I found with Plunkr and examples on how to recreate:


@Input (and potentially @Output) decorators are not inherited if the child component class specifies it's own @Input.

The code below has:

  1. an AbstractComponent with @Input a,
  2. a child class AComponent which correctly inherits that @Input a
  3. a child class BComponent which creates @Input b, and does not inherit @Input a

The expected output of this code should be a string, a a b. The actual output is just a b

Here is this bugs Plunkr example (all code is in app.module.ts).

import {Component, Input, NgModule} from 'ng-metadata/core';
import {platformBrowserDynamic} from 'ng-metadata/platform-browser-dynamic';

abstract class AbstractComponent {
    @Input('@') public a: string;
}

@Component({
    selector: 'aComponent',
    template: '<span>{{$ctrl.a}}</span>'
})
export class AComponent extends AbstractComponent  { }

@Component({
    selector: 'bComponent',
    template: '<span>{{$ctrl.a}} {{$ctrl.b}}</span>'
})
export class BComponent extends AbstractComponent  {
    @Input('@') public b: string;
}

@Component({
    selector: 'app',
    template: `
        <a-component a="a"></a-component>
        <b-component a="a" b="b"></b-component>
    `
})
export class AppComponent { }

@NgModule({
    declarations: [AComponent, BComponent, AppComponent],
    exports: [AppComponent],
    bootstrap: [AppComponent]
})
export default class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

@Host (and potentially @Self/@SkipSelf) constructor decorators incorrectly get applied to the parent class, adding parameter information to all child classes.

The code below has:

  1. an AbstractComponent with constructor @Inject('form') @Host of an IFormController,
  2. a child class AComponent which also @Inject('$http'), the IHttpService
  3. a child class BComponent which also @Inject('app') @Host the parent AppComponent.

I believe the BComponent's @Host on the second parameter is getting applied to every second parameter of any child of AbstractComponent and thus the $http services becomes a required directive.

Here is this bugs Plunkr example (all code is in app.module.ts).

This actually breaks Angular with an error logged as:

VM71 angular.js:14525 Error: [$compile:ctreq] Controller '$http', required by directive 'aComponent', can't be found!
http://errors.angularjs.org/1.6.4/$compile/ctreq?p0=%24http&p1=aComponent
    at eval (VM71 angular.js:66)
    at getControllers (VM71 angular.js:9896)
    at getControllers (VM71 angular.js:9903)
    at nodeLinkFn (VM71 angular.js:9796)
    at compositeLinkFn (VM71 angular.js:9055)
    at nodeLinkFn (VM71 angular.js:9809)
    at compositeLinkFn (VM71 angular.js:9055)
    at nodeLinkFn (VM71 angular.js:9809)
    at compositeLinkFn (VM71 angular.js:9055)
    at nodeLinkFn (VM71 angular.js:9809)
import {IFormController, IHttpService} from 'angular';
import {Component, Host, Inject, NgModule} from 'ng-metadata/core';
import {platformBrowserDynamic} from 'ng-metadata/platform-browser-dynamic';

abstract class AbstractComponent {
    private formController: IFormController;

    constructor (@Inject('form') @Host() formController: IFormController) {
        this.formController = formController;
    }
}

@Component({
    selector: 'aComponent'
})
export class AComponent extends AbstractComponent  {
    private $http: IHttpService;

    constructor (@Inject('form') @Host() formController: IFormController,
                 @Inject('$http') $http: IHttpService) {
        super(formController);
        this.$http = $http;
    }
}

@Component({
    selector: 'bComponent'
})
export class BComponent extends AbstractComponent  {
    private app: AppComponent;

    constructor (@Inject('form') @Host() formController: IFormController,
                 @Inject('app') @Host() app: AppComponent) {
        super(formController);
        this.app = app;
    }
}

@Component({
    selector: 'app',
    template: `
        <form>
            <a-component></a-component>
            <b-component></b-component>
        </form>
    `
})
export class AppComponent { }

@NgModule({
    declarations: [AComponent, BComponent, AppComponent],
    exports: [AppComponent],
    bootstrap: [AppComponent]
})
export default class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions