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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get a ViewContainerRef for a provider that is passed to bootstrap to load components #9293

Closed
pkozlowski-opensource opened this issue Jun 17, 2016 · 30 comments
Labels
area: core Issues related to the framework runtime feature Issue that requests a new feature freq1: low

Comments

@pkozlowski-opensource
Copy link
Member

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

[ ] bug report
[x ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

I can't get (inject) ViewContainerRefs for a top-level (application) component.

Expected/desired behavior

Should be able to inject ViewContainerRefs for a top-level (application) component as it might be needed to dynamically load a component from a service.

Reproduction of the problem

http://plnkr.co/edit/eM3ibINNHgGB7Y2GyC4i?p=preview

What is the motivation / use case for changing the behavior?

There is a class of widgets (modals, popovers, tooltips etc.) that:

  • need to loaded "dynamically"
  • are combination of content from a user (ex.: modal' content) + decoration around in a form of a component (ex. modal window)
  • are to be attached outside of a bootstraped application (ex. attach to body)
  • are best modeled as a service rather than a directive

Taking all the above we need to dynamically create a component (modal window) and provide projectable nodes (modal content). To make projectable nodes "live" we need a ViewContainerRef but since we are in a service we don't have access to any ViewContainerRef - hence the request.

@tbosch I was discussing this use-case yesterday with @mhevery and he advised to open an issue / discuss it with you.

Related: #8941

@tbosch
Copy link
Contributor

tbosch commented Jun 17, 2016

Could you update the description to mean "get a ViewContainerRef for a provider that is passed to bootstrap to load components"?

@tbosch
Copy link
Contributor

tbosch commented Jun 17, 2016

We can't create a ViewContainerRef, as a ViewContainerRef only exists within a view (e.g. in needs a parent AppElement which needs a parent AppView.

In general, your service needs a way to attach the change detector of a component to Angular. As far as I understand this issue, that should be enough. Could you confirm?

We already have ApplicationRef_.registerChangeDetector / unregisterChangeDetector. We probably should make these accessible on the abstract parent class ApplicationRef as well.

As a workaround, inject ApplicationRef, cast it to <any> and call registerChangeDetector on it.

@shlomiassaf
Copy link
Contributor

+1000, had the same issue with my modal component, had to force user to set a default ViewContainerRef since I couldn't get access to one.

Didn't want to use private method, @tbosch can you confirm it will go get exposed in the future and won't change?

@shlomiassaf
Copy link
Contributor

shlomiassaf commented Jun 18, 2016

@pkozlowski-opensource the suggestion from @tbosch should suffice.

However, this will force you to use direct DOM access to add that element, then use ComponentResolver -> ComponentFactory to create it since you can't access the ViewContainerRef, I have tested it and tried several methods to solve it, this is one of them.

Another option is to have the overlay as a root of another angular application via bootstrap.
That seems too much and it might have other effects... it does register the change detector as mentioned above.

@pkozlowski-opensource
Copy link
Member Author

In general, your service needs a way to attach the change detector of a component to Angular. As far as I understand this issue, that should be enough. Could you confirm?

@tbosch doesn't seem to be enough, unless I'm wrongly interpreting what you are saying. Here is what I've tried: http://plnkr.co/edit/HI6W52QtJ5Hc7zYR1KdU?p=preview

@shlomiassaf I don't want to hack around this use case, IMO it is common enough (modal, tooltips, popovers - any overly in general) that we want to have a canonical solution in the framework.

@shlomiassaf
Copy link
Contributor

shlomiassaf commented Jun 18, 2016

@pkozlowski-opensource I did some searching and found that there are several offered solutions to solve this, from different places - they all end up at the same block.

The main issue is that you can not get access to the ViewContainerRef outside of a component, any component.

In this case you're referring to the top-level root component, but it's the same to any component.
This issue is specific to ViewContainerRef of the top level component but it should be to any component.

I'm not sure using the DI is the way to go since there are 1-n root components - a token won't help.

#9254 for example, want's the same feature but from a ComponentRef, so a ComponentRef can play with the ViewContainerRef, in this case the change is just api exposure.

From there, if you have access to the App root ComponentRef you can get the ViewContainerRef.

@jelbourn hinted he is working on some sort of solution to this, angular/components#715 (comment)

To recap - expose the ViewContainerRef to the ComponentRef instance.

@pkozlowski-opensource
Copy link
Member Author

In this case you're referring to the top-level root component, but it's the same to any component.
This issue is specific to ViewContainerRef of the top level component but it should be to any component.

Not sure I follow. In any case we are really not that much interested in ViewContainerRef- we just need something that would allow us to load components and attach them under a DOM node of choice. So let's work this out with @tbosch without jumping to any conclusions about the final shape of a solution.

I'm not sure using the DI is the way to go since there are 1-n root components - a token won't help.

In this issue I was referring to "root component" as the one that is used to bootstrap an app and there can be only one component like this for any given app.

To recap - expose the ViewContainerRef to the ComponentRef instance.

I think it is backwards - you insert components inside view containers, not other way around.

Once again, let's not jump in all directions - there is a specific use-case and we need non-hacky solution.

@shlomiassaf
Copy link
Contributor

shlomiassaf commented Jun 18, 2016

@pkozlowski-opensource I'm not jumping into conclusions, i'm expressing my opinion. I'm not the one deciding so there's no harm here.

we just need something that would allow us to load components and attach them under a DOM node of choice

That's exactly ViewContainerRef

As a side note, ComponentRef has everything needed, including ViewComponentRef.

@kemsky
Copy link

kemsky commented Jun 19, 2016

I have created my own modal implementation and it requires 2 things:

  1. root view (or any other view) - to create/insert component.
  2. root injector (could be replaced with any child injector) - to get ComponentResolver reference and optionally to create child injector to pass parameters to modal which is also important.
PopupManager.instance.create(Class, [modal:boolean = true], [providers:Provider[] = []], [injector?:Injector]);

I'm not using templates, each modal is separate component with its own template. Currently i'm using static variables to share root view and injector, perhaps service is better idea.

Lack of dynamic content in you plunker could be solved using component inheritance. ng-content could help (as declarative way) but it is separate problem of its own (#8937 and others). Inheritance currently is also broken in many ways, so it will be much cleaner solution to use base popup class as container for dynamic components: load base class -> load modal class into base class.
I'm not sure if Problem1 is a problem, since you could implement cache if you want.


Adobe Flex framework used to have FlexGlobals class that had static topLevelApplication field and SystemManager/PopupManager singletons to manage popups, tooltips, overlays and etc., Java Swing has SwingUtilities class to get extra knowledge about component tree. HTML is different, using CSS you could turn anything into modal dialog.

@pkozlowski-opensource pkozlowski-opensource changed the title Expose top-level ViewContainerRef in DI Get a ViewContainerRef for a provider that is passed to bootstrap to load components Jun 20, 2016
@tbosch
Copy link
Contributor

tbosch commented Jun 20, 2016

@pkozlowski-opensource Your plunker shows 2 use cases:

  1. creating a component in a provider that has been passed to bootstrap
  2. creating an embedded view (from a TemplateRef) in a provider that has been passed to bootstrap

To fix your plunker, you need to register the change detectors of both!

See this plunker: http://plnkr.co/edit/tHlSZLObdAuCPZ3ZX6DY?p=preview

This uses the following non public API:

  • ApplicationRef_.registerChangeDetector is not public
  • Using ViewRef_ as a ChangeDetectorRef.

To fix this, we should:

  • move registerChangeDetector to ApplicationRef
  • expose a changeDetectorRef on EmbeddedViewRef as well

However, in general, these problems are only caused because the popup service is a provider passed to bootstrap. If it was a provider used on the root component, you can directly inject ViewContainerRef without a problem.

I.e. I am not sure whether we want to support this use case in general...

@tbosch
Copy link
Contributor

tbosch commented Jun 20, 2016

Also note that calling registerChangeDetectorRef might lead to a memory leak when using ComponentRef.destroy(), as it does not account for this case (yet).

@pkozlowski-opensource
Copy link
Member Author

@tbosch OK, I see now that you need to register both component and an embedded view change detectors, cheers for this!

However, in general, these problems are only caused because the popup service is a provider passed to bootstrap. If it was a provider used on the root component, you can directly inject ViewContainerRef without a problem.

But then would I still be able to move nodes outside of the bootstrapped application? Asking since with ComponentFactory::create I can specify rootSelectorOrNode while I don't see equivalent with ViewContainerRef::createComponent

@shlomiassaf
Copy link
Contributor

shlomiassaf commented Jun 20, 2016

ComponentFactory::create will search for the rootSelectorOrNode and treat it like a component (i.e compile the component)

ViewContainerRef::createComponent will add the new component as the last child of the host view, or in other word as the sibling of the ViewContainerRefs instance element

@shlomiassaf
Copy link
Contributor

shlomiassaf commented Jun 20, 2016

@pkozlowski-opensource I know you don't like it, but I'v spent hours on this and learned the linker by heart :) I have already tried all of the solutions above, and other workarounds.

Having ComponentRef expose the ViewContainerRef or at least the createComponent and createEmbeddedView functions - Will solve this issue

You will be able to compose a templateless component with multiple child-component from a service, without any issues and no need to declare directives on that parent component.

I'm 99% sure of this, but I will be happy to be wrong and get an answer, I'm dealing with dialogs since alpha 26 or something...

@tbosch just a simple question, if i'm creating a component using component factory, why is it that I can't get access to the ViewContainerRef, I understand AppElement is private but ViewContainerRef is out there...

@aminebizid
Copy link

If you will load such components dynamically how are you going to make template bindings as we used to use?

@shlomiassaf
Copy link
Contributor

@Zigzag95 see http://plnkr.co/edit/25o8wO7ruAAwKktJAggC?p=preview
This is just an illustration, in real world you will manually add components which are top level, i.e: no external template bindings but yes to local injected bindings. The data will enter through injection in the constructor.

Usually it should be some top level components added via code, the rest via templating system.

@HeavenlyHost
Copy link

I am having exactly the same issue, I have a service whereby I want to create my own modal component but have it placed at the end of the app component, but without the ViewContainerRef I am finding this to be impossible.

@dsara
Copy link

dsara commented Jul 5, 2016

Until this feature is implemented I've been using this to get the root ViewContainerRef. It's in no way the correct way to do it but it gets the job done.

class SomeComponent {
    constructor(private appRef: ApplicationRef) { }
    let viewRef: ViewContainerRef = appRef['_rootComponents'][0]['_hostElement'].vcRef;
}

@HeavenlyHost
Copy link

OK, I shall check one tomorrow. But I have to say there is a use case here and I do feel that an official solution should provided.

@HeavenlyHost
Copy link

@dsara I check your suggestion this morning and i may work, aside from the fact that I need to provide a dependancy for my service and it would appear to place the new component outside of the app component instead of within the component. If it were the latter then the provider already exists and there would not be a problem. It is essential that I am able to create the component within the scope of the app component, not concerned if it the last item or not, prefer it at the end though.

@dsara
Copy link

dsara commented Jul 6, 2016

@HeavenlyHost I've used this method to add a modal service to my application. It doesn't need to inherit any existing providers other than the ApplicaionRef. Originally I was unable to get the ApplicationRef provider by passing it to the constructor of the service because I'm loading the modal service during bootstrap so I had to provide the injector and then manually inject the ApplicationRef provider when opening the modal. I don't know if this method might allow you to get existing providers but I thought I'd post it and see if it might help you. If this doesn't work you might have to get one of the first child components below the app component and then load to next to that location instead of the root. It would be nice to have a function that instead of "loadNextToLocation" you could "loadWithinLocation" or something like that so you could dynamically load a component as a child of an existing component instead of next to it.

@Injectable()
export class ModalService {
    private currentModal: ModalComponent;

    constructor(private dynamicComponentLoader: DynamicComponentLoader, private injector: Injector) { }

    public openModal(): Promise<ModalComponent> {
        var appRef = <ApplicationRef>this.injector.get(ApplicationRef);
        let viewRef: ViewContainerRef = appRef['_rootComponents'][0]['_hostElement'].vcRef;
        return this.dynamicComponentLoader.loadNextToLocation(ModalComponent, viewRef).then((compRef: ComponentRef<ModalComponent>) => {
            this.currentModal = compRef.instance.modalInit(compRef);
            return this.currentModal;
        });
    }
}

@teleaziz
Copy link
Contributor

teleaziz commented Aug 4, 2016

+1

@amcdnl
Copy link
Contributor

amcdnl commented Oct 4, 2016

Facing a similar challenge, posted my question on SO and wanted to reference here - http://stackoverflow.com/questions/39857222/angular2-dynamic-component-injection-in-root

@vicb vicb added the freq1: low label Oct 6, 2016
tbosch added a commit to tbosch/angular that referenced this issue Nov 4, 2016
This feature is useful to allow components / embedded views
to be dirty checked if they are not placed in any `ViewContainer`.

Closes angular#9293
tbosch added a commit to tbosch/angular that referenced this issue Nov 4, 2016
This feature is useful to allow components / embedded views
to be dirty checked if they are not placed in any `ViewContainer`.

Closes angular#9293
tbosch added a commit to tbosch/angular that referenced this issue Nov 15, 2016
This feature is useful to allow components / embedded views
to be dirty checked if they are not placed in any `ViewContainer`.

Closes angular#9293
@vicb vicb closed this as completed in 9f7d32a Nov 15, 2016
alexeagle pushed a commit to alexeagle/angular that referenced this issue Nov 17, 2016
This feature is useful to allow components / embedded views
to be dirty checked if they are not placed in any `ViewContainer`.

Closes angular#9293
@amcdnl
Copy link
Contributor

amcdnl commented Nov 17, 2016

@vicb - I'm curious given my question above on SO and this new commit, if there is a better way now to address that issue. My hack is actually broken in 2.2 now as well.

@scttcper
Copy link

scttcper commented Nov 17, 2016

@amcdnl
Copy link
Contributor

amcdnl commented Nov 17, 2016

@scttcper - Ok that actually helped. I was able to abstract out all their specific logic and make this work standalone service. Here is the code for others:

import {
  ApplicationRef, ComponentFactoryResolver, ComponentRef, Injectable,
  Injector, ViewContainerRef, EmbeddedViewRef, Type
} from '@angular/core';

/**
 * Injection service is a helper to append components
 * dynamically to a known location in the DOM, most
 * noteably for dialogs/tooltips appending to body.
 * 
 * @export
 * @class InjectionService
 */
@Injectable()
export class InjectionService {
  private _container: ComponentRef<any>;

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

  /**
   * Gets the root view container to inject the component to.
   * 
   * @returns {ComponentRef<any>}
   * 
   * @memberOf InjectionService
   */
  getRootViewContainer(): ComponentRef<any> {
    if(this._container) return this._container;

    const rootComponents = this.applicationRef['_rootComponents'];
    if (rootComponents.length) return rootComponents[0];

    throw new Error('View Container not found! ngUpgrade needs to manually set this via setRootViewContainer.');
  }

  /**
   * Overrides the default root view container. This is useful for 
   * things like ngUpgrade that doesn't have a ApplicationRef root.
   * 
   * @param {any} container
   * 
   * @memberOf InjectionService
   */
  setRootViewContainer(container): void {
    this._container = container;
  }

  /**
   * Gets the html element for a component ref.
   * 
   * @param {ComponentRef<any>} componentRef
   * @returns {HTMLElement}
   * 
   * @memberOf InjectionService
   */
  getComponentRootNode(componentRef: ComponentRef<any>): HTMLElement {
    return (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
  }

  /**
   * Gets the root component container html element.
   * 
   * @returns {HTMLElement}
   * 
   * @memberOf InjectionService
   */
  getRootViewContainerNode(): HTMLElement {
    return this.getComponentRootNode(this.getRootViewContainer());
  }

  /**
   * Projects the inputs onto the component
   * 
   * @param {ComponentRef<any>} component
   * @param {*} options
   * @returns {ComponentRef<any>}
   * 
   * @memberOf InjectionService
   */
  projectComponentInputs(component: ComponentRef<any>, options: any): ComponentRef<any> {
    if(options) {
      const props = Object.getOwnPropertyNames(options);
      for(const prop of props) {
        component.instance[prop] = options[prop];
      }
    }

    return component;
  }

  /**
   * Appends a component to a adjacent location
   * 
   * @template T
   * @param {Type<T>} componentClass
   * @param {*} [options={}]
   * @param {Element} [location=this.getRootViewContainerNode()]
   * @returns {ComponentRef<any>}
   * 
   * @memberOf InjectionService
   */
  appendComponent<T>(
    componentClass: Type<T>, 
    options: any = {}, 
    location: Element = this.getRootViewContainerNode()): ComponentRef<any> {

    let componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass);
    let componentRef = componentFactory.create(this.injector);
    let appRef: any = this.applicationRef;
    let componentRootNode = this.getComponentRootNode(componentRef);

    // project the options passed to the component instance
    this.projectComponentInputs(componentRef, options);

    // ApplicationRef's attachView and detachView methods are in Angular ^2.2.1 but not before.
    // The `else` clause here can be removed once 2.2.1 is released.
    if (appRef['attachView']) {
      appRef.attachView(componentRef.hostView);

      componentRef.onDestroy(() => {
        appRef.detachView(componentRef.hostView);
      });
    } else {
      // When creating a component outside of a ViewContainer, we need to manually register
      // its ChangeDetector with the application. This API is unfortunately not published
      // in Angular <= 2.2.0. The change detector must also be deregistered when the component
      // is destroyed to prevent memory leaks.      
      let changeDetectorRef = componentRef.changeDetectorRef;
      appRef.registerChangeDetector(changeDetectorRef);

      componentRef.onDestroy(() => {
        appRef.unregisterChangeDetector(changeDetectorRef);

        // Normally the ViewContainer will remove the component's nodes from the DOM.
        // Without a ViewContainer, we need to manually remove the nodes.
        if (componentRootNode.parentNode) {
          componentRootNode.parentNode.removeChild(componentRootNode);
        }
      });
    }

    location.appendChild(componentRootNode);

    return componentRef;
  }
}

@patrickmichalina
Copy link

@amcdnl I seem to have gotten this working with the above code, but something is happening with change detection. Have you had a similar issue?

Expression has changed after it was checked. Previous value: 'CD_INIT_VALUE'. Current value: 'spinner-message'. It seems like the view has been created after its parent and its children have been dirty checked. Has it been created in a change detection hook ?

@amcdnl
Copy link
Contributor

amcdnl commented Nov 22, 2016

@patrickmichalina - I have not ran into this, shoot me a email amcdaniel2 at gmail

@patrickmichalina
Copy link

@amcdnl it appears this is because I have dynamicly rendered components within dynamically rendered components. Using the older, hacky method, everything worked fine, but now with Angular 2.2.0+ those are no longer available. Your code works on the first dynamic components, but failing for the nested ones (which doesn't make sense). So I think it is me using a combination of methods that is causing the problem. I will investigate further and post if the issue persists after I refactor using this newer appRef.attachView API

@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 Sep 11, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: core Issues related to the framework runtime feature Issue that requests a new feature freq1: low
Projects
None yet
Development

No branches or pull requests