Skip to content

Commit 89eac70

Browse files
jelbournmatsko
authored andcommitted
fix(ivy): remove DOM nodes from their real parent vs saved parent (angular#28455)
Currently, DOM node removal called `removeChild` on the saved parent node when destroying a component. However, this will fail if the component has been manually moved in the DOM. This change makes the removal always use the node's real `parentNode` and ignore the provided `parent`. PR Close angular#28455
1 parent 5a2c3ff commit 89eac70

File tree

5 files changed

+84
-21
lines changed

5 files changed

+84
-21
lines changed

packages/core/src/render3/i18n.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {RComment, RElement} from './interfaces/renderer';
1919
import {SanitizerFn} from './interfaces/sanitization';
2020
import {StylingContext} from './interfaces/styling';
2121
import {BINDING_INDEX, HEADER_OFFSET, HOST_NODE, LView, RENDERER, TVIEW, TView} from './interfaces/view';
22-
import {appendChild, createTextNode, removeChild} from './node_manipulation';
22+
import {appendChild, createTextNode, removeNode as removeRNode} from './node_manipulation';
2323
import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from './state';
2424
import {NO_CHANGE} from './tokens';
2525
import {addAllToArray, getNativeByIndex, getNativeByTNode, getTNode, isLContainer, renderStringify} from './util';
@@ -830,7 +830,7 @@ function removeNode(index: number, viewData: LView) {
830830
const removedPhTNode = getTNode(index, viewData);
831831
const removedPhRNode = getNativeByIndex(index, viewData);
832832
if (removedPhRNode) {
833-
removeChild(removedPhTNode, removedPhRNode, viewData);
833+
removeRNode(removedPhTNode, removedPhRNode, viewData);
834834
}
835835

836836
removedPhTNode.detached = true;
@@ -840,7 +840,7 @@ function removeNode(index: number, viewData: LView) {
840840
if (isLContainer(slotValue)) {
841841
const lContainer = slotValue as LContainer;
842842
if (removedPhTNode.type !== TNodeType.Container) {
843-
removeChild(removedPhTNode, lContainer[NATIVE], viewData);
843+
removeRNode(removedPhTNode, lContainer[NATIVE], viewData);
844844
}
845845
}
846846
}

packages/core/src/render3/node_manipulation.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ function executeNodeAction(
181181
if (action === WalkTNodeTreeAction.Insert) {
182182
nativeInsertBefore(renderer, parent !, node, beforeNode || null);
183183
} else if (action === WalkTNodeTreeAction.Detach) {
184-
nativeRemoveChild(renderer, parent !, node, isComponent(tNode));
184+
nativeRemoveChild(renderer, node, isComponent(tNode));
185185
} else if (action === WalkTNodeTreeAction.Destroy) {
186186
ngDevMode && ngDevMode.rendererDestroyNode++;
187187
(renderer as ProceduralRenderer3).destroyNode !(node);
@@ -593,13 +593,19 @@ function nativeAppendOrInsertBefore(
593593
}
594594
}
595595

596-
/**
597-
* Removes a native child node from a given native parent node.
598-
*/
596+
/** Removes a node from the DOM. */
599597
export function nativeRemoveChild(
600-
renderer: Renderer3, parent: RElement, child: RNode, isHostElement?: boolean): void {
601-
isProceduralRenderer(renderer) ? renderer.removeChild(parent as RElement, child, isHostElement) :
602-
parent.removeChild(child);
598+
renderer: Renderer3, child: RNode, isHostElement?: boolean): void {
599+
if (isProceduralRenderer(renderer)) {
600+
const renderParent = renderer.parentNode(child);
601+
if (renderParent) {
602+
renderer.removeChild(renderParent, child, isHostElement);
603+
}
604+
} else {
605+
// We intentionally don't use the given parent node since it may no longer
606+
// match the state of the DOM (if the child node has been manually moved).
607+
child.parentNode && child.parentNode.removeChild(child);
608+
}
603609
}
604610

605611
/**
@@ -692,14 +698,9 @@ export function getBeforeNodeForView(index: number, views: LView[], containerNat
692698
* @param childTNode The TNode of the child to remove
693699
* @param childEl The child that should be removed
694700
* @param currentView The current LView
695-
* @returns Whether or not the child was removed
696701
*/
697-
export function removeChild(childTNode: TNode, childEl: RNode, currentView: LView): void {
698-
const parentNative = getRenderParent(childTNode, currentView);
699-
// We only remove the element if it already has a render parent.
700-
if (parentNative) {
701-
nativeRemoveChild(currentView[RENDERER], parentNative, childEl);
702-
}
702+
export function removeNode(childTNode: TNode, childEl: RNode, currentView: LView): void {
703+
nativeRemoveChild(currentView[RENDERER], childEl);
703704
}
704705

705706
/**

packages/core/test/linker/view_injector_integration_spec.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, DebugElement, Directive, ElementRef, Host, Inject, InjectionToken, Injector, Input, NgModule, Optional, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewContainerRef} from '@angular/core';
9+
import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, DebugElement, Directive, ElementRef, EmbeddedViewRef, Host, Inject, InjectionToken, Injector, Input, NgModule, Optional, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewContainerRef} from '@angular/core';
1010
import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing';
1111
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
1212
import {expect} from '@angular/platform-browser/testing/src/matchers';
@@ -1021,6 +1021,58 @@ class TestComp {
10211021
expect(impurePipe4).not.toBe(impurePipe1);
10221022
});
10231023
});
1024+
1025+
describe('view destruction', () => {
1026+
@Component({selector: 'some-component', template: ''})
1027+
class SomeComponent {
1028+
}
1029+
1030+
@NgModule({
1031+
declarations: [SomeComponent],
1032+
exports: [SomeComponent],
1033+
entryComponents: [SomeComponent]
1034+
})
1035+
class SomeModule {
1036+
}
1037+
1038+
@Component({selector: 'listener-and-on-destroy', template: ''})
1039+
class ComponentThatLoadsAnotherComponentThenMovesIt {
1040+
constructor(
1041+
private viewContainerRef: ViewContainerRef,
1042+
private componentFactoryResolver: ComponentFactoryResolver) {}
1043+
1044+
1045+
ngOnInit() {
1046+
// Dynamically load some component.
1047+
const componentFactory =
1048+
this.componentFactoryResolver.resolveComponentFactory(SomeComponent);
1049+
const componentRef =
1050+
this.viewContainerRef.createComponent(componentFactory, this.viewContainerRef.length);
1051+
1052+
// Manually move the loaded component to some arbitrary DOM node.
1053+
const componentRootNode =
1054+
(componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
1055+
document.createElement('div').appendChild(componentRootNode);
1056+
1057+
// Destroy the component we just moved to ensure that it does not error during
1058+
// destruction.
1059+
componentRef.destroy();
1060+
}
1061+
}
1062+
1063+
it('should not error when destroying a component that has been moved in the DOM', () => {
1064+
TestBed.configureTestingModule({
1065+
imports: [SomeModule],
1066+
declarations: [ComponentThatLoadsAnotherComponentThenMovesIt],
1067+
});
1068+
const fixture =
1069+
createComponentFixture(`<listener-and-on-destroy></listener-and-on-destroy>`);
1070+
fixture.detectChanges();
1071+
1072+
// This test will fail if the ngOnInit of ComponentThatLoadsAnotherComponentThenMovesIt
1073+
// throws an error.
1074+
});
1075+
});
10241076
});
10251077
})();
10261078

packages/core/test/render3/view_container_ref_spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, ViewRef, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core';
9+
import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ComponentRef, defineInjector, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, ViewRef, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef,} from '../../src/core';
10+
import {createInjector} from '../../src/di/r3_injector';
1011
import {ViewEncapsulation} from '../../src/metadata';
11-
1212
import {AttributeMarker, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, listener, loadViewQuery, NgOnChangesFeature, queryRefresh, viewQuery,} from '../../src/render3/index';
1313

1414
import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementHostAttrs, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, nextContext, projection, projectionDef, reference, template, text, textBinding,} from '../../src/render3/instructions';
@@ -18,7 +18,6 @@ import {NgModuleFactory} from '../../src/render3/ng_module_ref';
1818
import {pipe, pipeBind1} from '../../src/render3/pipe';
1919
import {getLView} from '../../src/render3/state';
2020
import {getNativeByIndex} from '../../src/render3/util';
21-
import {createInjector} from '../../src/di/r3_injector';
2221
import {templateRefExtractor} from '../../src/render3/view_engine_compatibility_prebound';
2322
import {NgForOf} from '../../test/render3/common_with_def';
2423

packages/platform-browser/test/dom/dom_renderer_spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
8787
});
8888
});
8989

90+
describe('removeChild', () => {
91+
it('should not error when removing a child with a different parent than given', () => {
92+
const savedParent = document.createElement('div');
93+
const realParent = document.createElement('div');
94+
const child = document.createElement('div');
95+
96+
realParent.appendChild(child);
97+
renderer.removeChild(savedParent, child);
98+
});
99+
});
100+
90101
if (browserDetection.supportsDeprecatedShadowDomV0) {
91102
it('should allow to style components with emulated encapsulation and no encapsulation inside of components with shadow DOM',
92103
() => {

0 commit comments

Comments
 (0)