Skip to content

Entry Components of a Lazy Loaded NgModule are not available outside the Module #14324

Closed

Description

@marvinscharle

I'm submitting a ... (check one with "x")

[ ] bug report => search github for a similar issue or PR before submitting
[X] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Follow-up issue for #12275 as requested by @DzmitryShylovich

Current behavior
entryComponents of a lazy loaded NgModule cannot be rendered using ComponentFactoryResolver. Error message: No component factory found for {{entryComponent}}

Expected behavior
entryComponents should be available just like if the module is imported

Minimal reproduction of the problem with instructions
http://plnkr.co/edit/9euisYeSNbEFrfzOhhzf?p=preview

I created a simple setup, similar to what we use internally. Main component provides a method to render a Type. EntryComponent is declared as entryComponent in Page1Module. However after having loaded Page1Module, when trying to render EntryComponent via ComponentFactoryResolver, No component factory found for EntryComponent is thrown.

What is the motivation / use case for changing the behavior?
Render entryComponents of child modules at root level. Use cases for this approach are

  • Modals that shall be rendered on top of everything
  • Notifications
  • etc.

Please tell us about your environment:
We're using currently Angular 2.1.1 but this affects the latest version of Angular (2.4.6) as well (see plnkr).

  • Language: TypeScript ^2.0.0

Activity

  1. DzmitryShylovich commented on Feb 6, 2017

    @DzmitryShylovich
    Contributor

    After some investigation I figured out that in your in works as designed.
    Entry components follow the same rules as providers in lazy loaded modules. https://angular.io/docs/ts/latest/cookbook/ngmodule-faq.html#!#q-lazy-loaded-module-provider-visibility
    They are only visible inside lazy loaded module.

  2. marvinscharle commented on Feb 6, 2017

    @marvinscharle
    Author

    Ok, if this is the intended behavior, how can specific tasks like the use cases above be accomplished?

    I don't think that blowing up the main module is a proper solution.

  3. DzmitryShylovich commented on Feb 6, 2017

    @DzmitryShylovich
    Contributor

    If you want to use an entry component in MainComponent then you should provide it in AppModule

  4. marvinscharle commented on Feb 6, 2017

    @marvinscharle
    Author

    So your solution indeed is to move everything into the main module?

    IMO this totally breaks the module concept of Angular which should allow to group functionality blocks into one module.

    This means basically that things like modals are impossible to use with a lazy load architecture.

  5. DzmitryShylovich commented on Feb 6, 2017

    @DzmitryShylovich
    Contributor

    You need to use an api similar to material's https://material.angular.io/components/component/dialog
    Then it should work

  6. marvinscharle commented on Feb 6, 2017

    @marvinscharle
    Author

    Wow, so your opinion is that we should rather move DOM content, like Angular Material does. See here: https://github.com/angular/material2/blob/master/src/lib/core/portal/dom-portal-host.ts#L30-L55

    If this is really the best practice to handle this in Angular, basically we can dump Angular and start using JQuery again.

    Sorry, but I cannot take this approach seriously.

  7. DzmitryShylovich commented on Feb 6, 2017

    @DzmitryShylovich
    Contributor

    I don't know what is the best approach for modals and lazy loading, but anyway in your plunkr it works as intended and there's no angular bug here.
    For discussion about modals it's better to use support channels such as gitter and stackoverflow

  8. marvinscharle commented on Feb 6, 2017

    @marvinscharle
    Author

    Ok, in this case I'm changing this issue into a feature request.

    IMO these use cases should be handled by Angular itself rather than by moving things along the DOM manually.

  9. artaommahe commented on Mar 6, 2017

    @artaommahe
    Contributor

    Faced with same issue today - component for modal defined in lazy loaded module does not handled by app modal service (it cant find it despite entryComponents usage). Forced to move it to main bundle that breaks lazy modules structure and usage logic 😕

  10. kiwileaks78 commented on Mar 7, 2017

    @kiwileaks78

    Same issue there ... Impossible to use an entryComponents in a lazyLoaded module without breaking the lazy modules logic : My LazyLoaded module depends on an entryComponent declared in my AppModule :(

  11. jmcclanahan commented on Mar 13, 2017

    @jmcclanahan

    Ran into this same issue, I did come to a resolution though after looking through ng-bootstrap modal source code. Essentially the issue (from my understanding please correct me if I'm wrong) is that your lazy modules are not included in the initial injector when your app is bootstrapped and the initial injector cannot be modified once it is set. Which is why you get the error in the OP. When one of your lazy modules is loaded it gets its own injector with references to whatever you declare or provide in your lazy module. That being said, as long as wherever you create your dynamic component at has reference to the correct injector and componentFactoryResolver you can indeed reference an entryComponent outside of the lazy loaded module.

    So, what I did was create a singleton service to store the injector and componentFactoryResolver of the dynamic component I want to create. This service needs to be outside of your lazy modules. Then whenever I go to dynamically create a component I can call this service to get what I need.

    Code below will be wherever you are creating your dynamic component outside of your lazy module

    @ViewChild('parent', {read: ViewContainerRef}) parent: ViewContainerRef;
      private componentRef: ComponentRef<any>;
    ...
    const childComponent = this.componentFactoryResolver.resolveComponentFactory(yourComponentRef);
    this.refInjector = ReflectiveInjector.resolveAndCreate([{provide: yourComponentRef, useValue: yourComponentRef}], this.injector);
    this.componentRef = this.parent.createComponent(childComponent, 0, this.refInjector);
    <div #parent></div>

    Then in your parent component for your entryComponent you'll inject injector and componentFactoryResolver and set their values in the shared service:

    constructor(private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector) {}

    Let me know if this doesn't make any sense and I can elaborate further. Edit I forgot to mention I'm using Angular 2.4.9.

  12. gcorgnet commented on Mar 20, 2017

    @gcorgnet

    Thanks @jmcclanahan. Are you able to tell us a bit more about what that service looks like and how it is set up? I am struggling with a similar issue, trying ti dynamically load a component from one lazy loaded module into a component from another lazy-loaded module and even though (I think) I have added the entry components in the right place, I get told I haven't. Thanks

  13. jmcclanahan commented on Mar 20, 2017

    @jmcclanahan

    @gcorgnet - Sure. So my use case was, I have a common toolbar on every page of my app, but within the toolbar I wanted to add buttons and tools for additional functionality based on what page I was on. In order to achieve this I created a component to hold these buttons/tools and their logic and I wanted to keep these components inside of the module they are associated with. So, essentially nothing outside of my module knows anything about these specific toolbar functions while still being able to show them on the common toolbar. Below is my working solution which I hope will help you:

    All I'm doing with the toolbar service is creating an observable, so your lazy component can pass it's componentFactoryResolver and injector references to the toolbar common component that is listening for the receiveContext Event.

    toolbar.service.ts

    @Injectable()
    export class ToolbarService {
        contextReceivedSource = new Subject<any>();
        contextReceived$ = this.contextReceivedSource.asObservable();
    
        receiveContext(componentFactoryResolver: ComponentFactoryResolver, injector: Injector) {
            this.contextReceivedSource.next({ resolver: componentFactoryResolver, injector: injector });
        }
    }

    In your lazy loaded component you'll want to inject componentFactoryResolver and injector and fire an event in the toolbar service.

    lazy loaded toolbar component

    ...
    constructor(private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector) {}
    ...
    ngOnInit() {
      this.toolbarService.receiveContext(this.componentFactoryResolver, this.injector);
    }
    ...

    Finally, in my core toolbar component I'm subscribing to the observable in the toolbar service so that anytime an event is fired it will try to inject and create the lazy component that I need. It's also important to destroy the componentRef that you create otherwise you will end up with memory leaks.

    ...
    @ViewChild('toolbarTarget', {read: ViewContainerRef}) toolbarTarget: ViewContainerRef;
    component: Type<Component>;
    refInjector: ReflectiveInjector;
    resolverSub: Subscription;
    refInjector: ReflectiveInjector;
    componentFactoryResolver: ComponentFactoryResolver;
    injector: Injector;
    
    ngOnInit() {
      this.resolverSub = this.toolbarService.contextReceived$.subscribe(resolver => {
          this.componentFactoryResolver = resolver.resolver;
          this.injector = resolver.injector;
        });
    }
    
    updateToolbar(data: any) {
        this.clearComponent();
        this.component = data['toolbar'];
        if (this.component) {
          const childComponent = this.componentFactoryResolver.resolveComponentFactory(this.component);
          this.refInjector = ReflectiveInjector
              .resolveAndCreate([{provide: this.component, useValue: this.component}], this.injector);
          this.componentRef = this.toolbarTarget.createComponent(childComponent, 0, this.refInjector);
        }
      }
    
      clearComponent() {
        this.toolbarTarget.clear();
        if (this.componentRef) { this.componentRef.destroy(); };
      }
    
      ngOnDestroy() {
        this.resolverSub.unsubscribe();
        this.clearComponent();
      }

    Your dynamically created component will then be placed wherever you put the corresponding #toolbarTarget

    <div #toolbarTarget></div>

    As an aside, I'm using this line this.component = data['toolbar']; in the common toolbar component to get the lazy loaded component reference from the route. If you want to see the full code for this I can post that, but it's outside the scope of this discussion.

    { path: '', component: yourComponent, data: { toolbar: yourToolbarComponentToInject } }

    Let me know if you have any further questions!

  14. added
    featureLabel used to distinguish feature request from other issues
    on Mar 21, 2017
  15. Dunos commented on May 4, 2017

    @Dunos

    @jmcclanahan I have one question (probably a stupid one) about your code, when you mention the "lazy loaded toolbar component", you put the service call code in the ngOnInit section, but how do you manage to initialize it (the component) without having to add it to the HTML code and hide it later in the DOM? Since that component is only going to be used in the topbar, I don't load it anywhere else before.

  16. 99 remaining items

  17. denkerny commented on Nov 27, 2019

    @denkerny

    looks like it doesnt work with --prod mode

  18. broweratcognitecdotcom commented on Nov 27, 2019

    @broweratcognitecdotcom

    Even if it did work, it does not address the REAL problem. the problem is if I add a component to the entryComponents of an NgModule, and that module is imported into a lazily loaded module in my app, then the entryComponents should automatically get registered with the apps entry components. There should be no further work required. I would further say that any component in the entryComponents of any NgModule should automatically wind up in the entryComponents of the app module. This is currently not the case. If it were then we wouldn't be here.

  19. mlc-mlapis commented on Nov 27, 2019

    @mlc-mlapis
    Contributor

    @broweratcognitecdotcom What about conflicts and priorities from different injection levels?

  20. Dzhuneyt commented on Nov 27, 2019

    @Dzhuneyt

    @mlc-mlapis order should be on a first-come, first-served basis in my opinion.

  21. broweratcognitecdotcom commented on Nov 28, 2019

    @broweratcognitecdotcom

    @mlc-mlapis Do you mean if different components have the same name? First match or an error if there are multiple matches. I could live with a requirement that entry component names must be unique.

  22. mlc-mlapis commented on Nov 28, 2019

    @mlc-mlapis
    Contributor

    @broweratcognitecdotcom Hi, yes. Because all variants have to be covered if we are talking about general principles.

  23. broweratcognitecdotcom commented on Nov 28, 2019

    @broweratcognitecdotcom

    @mlc-mlapis I think that the limitation in angular that many would like to overcome is that entry components can only be instantiated and placed in the dom by the app. The app does not need to know about a dialog box that a module that I import into a lazy loaded module uses. The current concept of entry components breaks the modular pattern of angular.

  24. Airblader commented on Nov 28, 2019

    @Airblader
    Contributor

    AFAIK with Ivy entryComponents will no longer be necessary, right? Since components will have locality so just dynamically importing them will always work?

  25. mlc-mlapis commented on Nov 28, 2019

    @mlc-mlapis
    Contributor

    @Airblader You are right, we know. It was just a small reminiscence back to history. 😄

  26. th0r commented on Nov 28, 2019

    @th0r

    For those who still struggling with this issues here is the solution that actually works: #14324 (comment)

  27. Jimmysh commented on Dec 4, 2019

    @Jimmysh

    I publish a package for this issues. some code copy from jonrimmer.
    It looks like that the issue does not exist in Angular 9. If you need fix this now. you can use @aiao/lazy-component
    sample code here
    https://github.com/aiao-io/aiao/tree/master/integration/lazy-component

  28. pkozlowski-opensource commented on Mar 11, 2020

    @pkozlowski-opensource
    Member

    I believe that this is fixed in Angular v9 running ivy. If anyone still faces similar problems, please open a new issue with a minimal reproduce scenario. Thnx!

  29. angular-automatic-lock-bot commented on Apr 11, 2020

    @angular-automatic-lock-bot

    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.

  30. locked and limited conversation to collaborators on Apr 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimefeatureLabel used to distinguish feature request from other issues

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions