Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom decorators not working again #39758

Closed
leonelvsc opened this issue Nov 19, 2020 · 5 comments
Closed

Custom decorators not working again #39758

leonelvsc opened this issue Nov 19, 2020 · 5 comments

Comments

@leonelvsc
Copy link
Contributor

leonelvsc commented Nov 19, 2020

馃悶 bug report

Affected Package

The issue is caused by package @angular/compiler

Is this a regression?

Yes, the previous version in which this bug was not present was: 11.0.0

Description

When i updated the angular packages to 11.0.1 version the custom decorators on my project stopped working, same behaviour as before 11.0.0 i think this issue is linked with this one #39574

the code:

type-manager.decorator.ts

const TYPE_MAP = new Map<string, any>();

export function TypeManagerDecorator(aType: string) {
    return function _TypeManagerDecorator<T extends { new(...args: any[]): TypeManager}>(constr: T) {
        TYPE_MAP.set(aType, constr);
    };
}

export function getComponent(aType: string) {
  return TYPE_MAP.get(aType);
}

then the component register to that map like this

manager-for-type-a.component.ts

@TypeManagerDecorator('a')
@Component({
    selector: 'app-manager-for-type-a',
    templateUrl: './manager-for-type-a.component.html',
    styleUrls: ['./manager-for-type-a.component.css'],
    viewProviders: [
        {
            provide: ControlContainer,
            useExisting: NgForm
        }
    ]
})
export class ManagerForTypeAComponent implements OnInit, TypeManager {

    constructor() {
    }

    ngOnInit() {
    };

    someMethod() {
    };
}

manager-for-type-b.component.ts

@TypeManagerDecorator('b')
@Component({
    selector: 'app-manager-for-type-b',
    templateUrl: './manager-for-type-b.component.html',
    styleUrls: ['./manager-for-type-b.component.css'],
    viewProviders: [
        {
            provide: ControlContainer,
            useExisting: NgForm
        }
    ]
})
export class ManagerForTypeBComponent implements OnInit, TypeManager {

    constructor() {
    }

    ngOnInit() {
    };

    someMethod() {
    };
}

manager

main.component.html

<form #form="ngForm" (ngSubmit)="form.valid" autocomplete="off">

    <select [(ngModel)]="type" name="type" (ngModelChange)="changeTypeManager($event)">
        <option value="a">Type A</option>
        <option value="b">Type B</option>
    </select>


    <ng-container #container></ng-container>

</form>

main.component.ts

@Component({
    selector: 'app-main',
    templateUrl: './main.component.html',
    styleUrls: ['./main.component.css']
})
export class MainComponent implements OnInit, OnDestroy {

    @ViewChild('container', {read: ViewContainerRef, static: true})
    private mainContainer: ViewContainerRef;
    private componentReference: ComponentRef<any>;

    constructor(
        private resolver: ComponentFactoryResolver
    ) {

    }

    ngOnInit() {
    }

    ngOnDestroy() {
        if(this.componentReference) {
            this.componentReference.destroy();
        }
    }

    private changeTypeManager(aType: string) {
        if(this.componentReference) {
            this.componentReference.destroy();
        }

        const component = getComponent(aType);

        if(!component) {
            throw new Error(`No class exists with the decorator @TypeManagerDecorator('${aType}')`);
        }

        const componentFactory = this.resolver.resolveComponentFactory(component);
        this.componentReference = this.mainContainer.createComponent(componentFactory);
    }

}

馃敟 Exception or Error

Decorators are not executing

馃實 Your Environment

Angular Version:

Working:




Angular CLI: 11.0.2
Node: 12.16.2
OS: linux x64

Angular: 11.0.0
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
... service-worker
Ivy Workspace: Yes

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1100.2
@angular-devkit/build-angular   0.1100.2
@angular-devkit/core            11.0.2
@angular-devkit/schematics      11.0.2
@angular/cli                    11.0.2
@angular/fire                   6.1.1
@angular/language-service       11.0.1
@schematics/angular             11.0.2
@schematics/update              0.1100.2
ng-packagr                      11.0.2
rxjs                            6.6.3
typescript                      4.0.5


Not Working:


Angular CLI: 11.0.2
Node: 12.16.2
OS: linux x64

Angular: 11.0.1
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router, service-worker
Ivy Workspace: Yes

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1100.2
@angular-devkit/build-angular   0.1100.2
@angular-devkit/core            11.0.2
@angular-devkit/schematics      11.0.2
@angular/cli                    11.0.2
@angular/fire                   6.1.1
@schematics/angular             11.0.2
@schematics/update              0.1100.2
ng-packagr                      11.0.2
rxjs                            6.6.3
typescript                      4.0.5


Anything else relevant?
Meaby the reason are the latest changes to the compiler

@JoostK
Copy link
Member

JoostK commented Nov 19, 2020

Heya, you're likely seeing your decorated components to be tree-shaken away, because there may not be any reference to them. The decorator executes a global side-effect and this setup cannot work with certain optimizations enabled.

My suggestion would be to turn off optimization/buildOptimizer in angular.json for the target you're building. If you want to continue using build optimizations, you'll have to explore an alternative way of registering the classes, i.e. through Angular's DI mechanism using a multi-provider. That way, the components are referenced from an NgModule's providers list which prevents them from being tree-shaken, and the registration would no longer rely on global side-effects.

I'll go ahead and close this as non-actionable from a framework perspective, but feel free to continue the discussion/ask questions below.

@JoostK JoostK closed this as completed Nov 19, 2020
@leonelvsc
Copy link
Contributor Author

leonelvsc commented Nov 19, 2020

@JoostK How can i provide an injection token from the components but resolved on a parent component?

I ask because the type managers and the main component reside in the same module, but i want to construct the references from the components and then inject them into the main component.

If i declare in any type manager component

providers: [
    {
      provide: TYPE_MANAGER_TOKEN,
      useValue: {'someType': SomeComponent},
      multi: true
    }
  ]

then inject it on the main component i get an empty array, of course that is because each component has it's own injector visibility "from here to childs"

I looked up the docs and found the @Host , @Optional , @Self , @SkipSelf but i couldn't found a way to provide an injection token on a child and inject it on the parent


[EDIT]
So i ended up building a static object map in the module declaration and injecting that object in the main component, is not the idea but it works of course

@JoostK
Copy link
Member

JoostK commented Nov 19, 2020

So what you're trying to do is conceptually incompatible with how tree-shaking works.

but i want to construct the references from the components

This requires that the component is "seen" somehow for the registration to be taken into account. If the component class is not imported from anywhere, then a tree-shaker considers it dead code and allows the class to be omitted from the build output.

What I meant to suggest was to provide the token from an NgModule:

@NgModule({
   providers: [
    { provide: TYPE_MANAGER_TOKEN, useValue: {'someType': SomeComponent}, multi: true },
  ]
})

This can be cleaned up with a function:

export function provideComponent(name: string, type: unknown) {
  return { provide: TYPE_MANAGER_TOKEN, useValue: {[name]: type}, multi: true };
}

@NgModule({
   providers: [
    provideComponent('someType', SomeComponent),
  ]
})

As an alternative, it is possible to stick with your original design using the decorator if you introduce a hard reference to the classes that have this decorator. This can be done using a static field on an NgModule class:

@NgModule({...})
export class SomeModule {
  static decoratedComponents: [SomeComponent],
}

Although that static field may not be read by anyone, its existence will prevent the tree-shaker from removing the SomeComponent from the build output.

@leonelvsc
Copy link
Contributor Author

leonelvsc commented Nov 19, 2020

Thank you so much for your time replying this issue, i went ahead with a static map in the module that the components reside in

{
    provide: TYPE_TOKEN,
    useValue: {
      'a': aManagerComponent,
      'b': bManagerComponent,
      'c': cManagerComponent,
      ...
    }
  }

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Dec 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants