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

Projected content is not hidden when dynamically creating a component if there is nowhere to project the content yet #54881

Open
nolan-white opened this issue Mar 15, 2024 · 4 comments
Labels
Milestone

Comments

@nolan-white
Copy link

nolan-white commented Mar 15, 2024

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

When creating a component dynamically via ViewContainerRef.createComponent(), any content children created via ViewContainerRef.createEmbeddedView() and provided through options.projectableContent are not detached from the document if the dynamically created component does not have a location to project content to yet. Instead, they remain attached to the document outside of the component they were supposed to be projected into. If this is not the intended way to do this, please correct me, but this was the only way I found to data bind the embedded view to the component its template was declared in.

function render<C>(
  componentType: Type<C>,
  viewContainerRef: ViewContainerRef,
  templateRef: TemplateRef<any>
): void {
  const embeddedViewRef = viewContainerRef.createEmbeddedView(templateRef);
  viewContainerRef.createComponent(componentType, {
    projectableNodes: [embeddedViewRef.rootNodes],
  });
}

This situation arises with components that don't project content, or conditionally project content. For components with conditionally projected content specifically, once there is a location to project the content, the content will be moved there. If that location becomes unavailable again later, the projected content is properly detached from the document.

image

I expect projecting content in dynamically created components to be consistent with projecting content declaratively, which will always detach the content to project from the document if there is nowhere to project it. If this inconsistency is intentional, then I expect there to be a documented solution for replicating the behavior of projecting content declaratively when dynamically creating a component.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/projected-content-not-hidden-dynamic-components?file=src%2Fapp%2Fapp.component.html

Please provide the exception or error you saw

N/A. No errors in console.

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 16.2.12
Node: 16.20.2
Package Manager: npm 9.8.1
OS: win32 x64

Angular: 16.2.12
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1602.12
@angular-devkit/build-angular   16.2.0
@angular-devkit/core            16.2.12
@angular-devkit/schematics      16.2.12
@angular/cdk                    16.2.14
@angular/material               16.2.14
@schematics/angular             16.2.12
rxjs                            7.8.1
typescript                      5.1.6
zone.js                         0.13.3

Anything else?

There is a single workaround for both the no projectable content and conditionally projectable content cases, which is to use Renderer2 to manually detach the elements created in the embedded view from the document before providing them to the component. I have provided examples of this workaround in the linked StackBlitz.

function renderWorkaround<C>(
  componentType: Type<C>,
  viewContainerRef: ViewContainerRef,
  templateRef: TemplateRef<any>,
  renderer: Renderer2
): void {
  const embeddedViewRef = viewContainerRef.createEmbeddedView(templateRef);
  for (const node of embeddedViewRef.rootNodes) {
    renderer.removeChild(node.parentNode, node);
  }
  viewContainerRef.createComponent(componentType, {
    projectableNodes: [embeddedViewRef.rootNodes],
  });
}

image

I am aware that this issue could theoretically be solved by using the suggested alternative to <ng-content> for conditional content projection, but my attempted solution of passing TemplateRef.element.nativeElement (which is just a comment) in options.projectableNodes when creating the component that projects content would not let me retrieve the TemplateRef via @ContentChild later.

I discovered this issue while implementing the longstanding workaround for cdkDragHandle being ignored when it is placed inside a child component for my dynamically created components. I figure it is worth mentioning as that issue was closed without an official solution over 5 years ago on the basis that it was not possible at the time. It might be worth revisiting if it is possible now.

@alxhub
Copy link
Member

alxhub commented Mar 16, 2024

The contract for createComponent and content projection is not well documented, but I believe that we expect/assume that any projected DOM nodes provided are not attached.

A good reason for this is performance. Often manually created DOM is never attached to begin with, so having the responsibility lie with the consumer avoids Angular needing to check if each node is attached, just in case.

@ngbot ngbot bot modified the milestone: needsTriage Mar 16, 2024
@nolan-white
Copy link
Author

The contract for createComponent and content projection is not well documented, but I believe that we expect/assume that any projected DOM nodes provided are not attached.

A good reason for this is performance. Often manually created DOM is never attached to begin with, so having the responsibility lie with the consumer avoids Angular needing to check if each node is attached, just in case.

In that case, is there a way to dynamically create components or embedded views with data binding without automatically attaching them to the DOM? As I mentioned, it seemed like the only way to get data binding to work with an embedded view was to create it via ViewContainerRef.createEmbeddedView(), which also automatically attaches the embedded view to the document. I tried creating it via TemplateRef.createEmbeddedView() which doesn't automatically attach it, but data binding to the outer component didn't happen. I thought the context argument of that function might help but it seems like that is a separate thing.

@alxhub
Copy link
Member

alxhub commented Apr 3, 2024

Are you attaching the component to change detection at all? ApplicationRef.attachView() is how you attach a view to change detection directly (without inserting it into a specific view container).

@nolan-white
Copy link
Author

I had not done that when using TemplateRef.createEmbeddedView(). I'll have to try that out and see if it works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants