26

This question is the Angular version of this question.

The following snippet summarizes my first attempt:

class NewParentComponent {

  constructor(elRef: ElementRef) {
    elRef.nativeElement.appendChild(myMovableElement);
    // where the reference to myMovableElement may come from a service.
  }
}

This attempt comes with the following issues:

  1. We are not supposed to manipulate the DOM directly in Angular. Is moving a component achievable with the Renderer somehow? (Ans: Per cgTag's answer, use Renderer2).
  2. If the original parent component gets destroyed after the child component was moved, the ngOnDestroy method is called on the child component.

See this Plunker demonstrating the issue. This Plunker is built on top of the Angular Dynamic Component Loader example. Open the browser console log and note that the "destroyed!" text is logged every time the ad banner changes, even when the "Drag Me!" text was dragged into the drop box.

Sam Herrmann
  • 6,293
  • 4
  • 31
  • 50
  • do you need to move components or simply DOM nodes? if components, how do you create them? – Max Koretskyi Jul 13 '17 at 05:13
  • I need to move (drag/drop) a child component (not just a node) from one parent component to another. Once the child component is moved, the original parent component may get destroyed and should not call the `ngOnDestroy` method on the child component. – Sam Herrmann Jul 13 '17 at 15:29
  • okay, do you create and inject them dynamically? create a plunker. Renderer will not help you with moving components – Max Koretskyi Jul 13 '17 at 15:41
  • I'll try to put together a concise Plunker, but the parent component is created and destroyed using [this approach](https://angular.io/guide/dynamic-component-loader#loading-components). Looking at this example, imagine a component that is inside an ad banner that can be drag/dropped outside of the ad banner. Once that component is moved outside of the ad banner, you do not want the `ngOnDestroy` to be called on that component when the ad banner is destroyed. I hope this helps! – Sam Herrmann Jul 13 '17 at 16:49
  • yeah, that's the right approach, well, create the most simplistic plunker, I'll take a look – Max Koretskyi Jul 13 '17 at 16:50
  • Ok I created a [Plunker](https://embed.plnkr.co/5FQ472Dhgf7QXYKx28sv/) that it built on top of the [Angular dynamic components](https://angular.io/guide/dynamic-component-loader) example with the addition of the drag/drop functionality. When you start the Plunker, open the browser dev tools and monitor the console. Every time the ad banner changes, you will see a "destroyed!" console log. The point of interest is that this console log will still get printed when the banner changes if you drag the "Drag Me!" text from inside the ad banner outside into the drop box. – Sam Herrmann Jul 13 '17 at 18:39
  • Please ignore the errors in the console at startup since they originate from the original [Angular example](https://angular.io/generated/live-examples/dynamic-component-loader/eplnkr.html) and are not due to my code change. – Sam Herrmann Jul 13 '17 at 18:42

3 Answers3

17

Moving a child component from one parent component to another can be achieved using ViewContainerRef:

oldParentViewContainerRef.detach(index);
newParentViewContainerRef.insert(viewRef);

The index is the index of the child component's view within the view-container. That can be obtained using the following:

let index = oldParentViewContainerRef.indexOf(viewRef);

The viewRef corresponds to the view of the child component. You can only get a hold of viewRef, if the child component was created programmatically with the help of ComponentFactoryResolver (see edit below for Angular v13):

let factory = componentFactoryResolver.resolveComponentFactory(MyChildComponentType);
let componentRef = oldParentViewContainerRef.createComponent(factory);
let viewRef = componentRef.hostView;

The takeaway here is, if you want to be able to move components between different parent components, make sure you create those movable components programmatically, not statically in the HTML. This gives you a ViewRef instance of those movable components, which works nicely together with ViewContainerRef.

See working Plunker for an example.

Edit: Angular v13

As of Angular v13, ComponentFactoryResolver is no longer needed to create components programmatically. Components can now be created as follows:

let componentRef = oldParentViewContainerRef.createComponent(MyChildComponentType);
let viewRef = componentRef.hostView;
Sam Herrmann
  • 6,293
  • 4
  • 31
  • 50
8

Yes, you can do this with the Renderer2 class and it is the preferred way. The reason for using the class is to keep your application compatible with Angular Universal.

Angular is not coupled to the DOM tree like it is in AngularJS. Angular now creates ViewRef objects which handle the coupling between a component and the DOM. This means that the native element used by the ViewRef can be moved around without breaking the change detention tree or the injection tree.

While the Renderer2 class is limited in API methods. It does have the methods you need.

abstract appendChild(parent: any, newChild: any): void;
abstract insertBefore(parent: any, newChild: any, refChild: any): void;
abstract removeChild(parent: any, oldChild: any): void;

I wrote a UI library that has windows as components. Those windows can dock into parent window panels. The only way I could do it was to move the DOM element for the component.

My only concern is the life-cycle for the component. When my windows destroy they move those elements back to where they were. I don't know if ngOnDestroy will be called if the element is removed from the DOM by a different parent.

Reactgular
  • 52,335
  • 19
  • 158
  • 208
  • Thanks @ThinkingMedia. I overlooked the deprecation note in the [Renderer documentation](https://angular.io/api/core/Renderer) and didn't even look at [Renderer2](https://angular.io/api/core/Renderer2). Your experience with your UI library and lifecycle concern actually very much sounds like my use case. Even when moving the component with Renderer2 I am having a lifecycle issue. My situation is as follows: – Sam Herrmann Jul 13 '17 at 15:16
  • 2
    I have 2 parent components: WindowA and WindowB. WindowA gets created and destroyed dynamically. When WindowA is created (i.e. opened), the user can move (drag/drop) child components into WindowB. When WindowA gets destroyed (i.e. closed), the ngOnDestroy method still is called in the child component that was moved into WindowB. – Sam Herrmann Jul 13 '17 at 15:20
  • @SamHerrmann In my application I have an auxiliary hidden component that is the owner of the movable components. When the movable component is to be displayed, so it is "borrowed" by a visible parent and later returned to its owner that is possibly responsible for its destroying. – David L. Mar 26 '18 at 12:31
-2

Manipulating the DOM is not recommended, but is not necessarily wrong. For instance, complex UI components will almost certainly need to manipulate the DOM directly (certain implementations of dialogs and drop downs, for instance). The thing to avoid is leaks, where the developer manipulates DOM without angular, and fails to cleanup the DOM after the manipulation is no longer needed.

When moving component's around in the DOM, I've found it most helpful to move it around inside a wrapper. For instance, when I include components in a modal component, the modal's parent element is in the DOM nested at the level where the component is added, and the modal's child elements are appended to the body tag. When the modal needs to be closed, the dialog child elements are appended back into the wrapper, taking the component back into the wrapper with them.