fix(ivy): remove DOM nodes from their real parent vs saved parent (#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 #28455
This commit is contained in:
parent
5a2c3ff8b5
commit
89eac702b5
@ -19,7 +19,7 @@ import {RComment, RElement} from './interfaces/renderer';
|
|||||||
import {SanitizerFn} from './interfaces/sanitization';
|
import {SanitizerFn} from './interfaces/sanitization';
|
||||||
import {StylingContext} from './interfaces/styling';
|
import {StylingContext} from './interfaces/styling';
|
||||||
import {BINDING_INDEX, HEADER_OFFSET, HOST_NODE, LView, RENDERER, TVIEW, TView} from './interfaces/view';
|
import {BINDING_INDEX, HEADER_OFFSET, HOST_NODE, LView, RENDERER, TVIEW, TView} from './interfaces/view';
|
||||||
import {appendChild, createTextNode, removeChild} from './node_manipulation';
|
import {appendChild, createTextNode, removeNode as removeRNode} from './node_manipulation';
|
||||||
import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from './state';
|
import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from './state';
|
||||||
import {NO_CHANGE} from './tokens';
|
import {NO_CHANGE} from './tokens';
|
||||||
import {addAllToArray, getNativeByIndex, getNativeByTNode, getTNode, isLContainer, renderStringify} from './util';
|
import {addAllToArray, getNativeByIndex, getNativeByTNode, getTNode, isLContainer, renderStringify} from './util';
|
||||||
@ -830,7 +830,7 @@ function removeNode(index: number, viewData: LView) {
|
|||||||
const removedPhTNode = getTNode(index, viewData);
|
const removedPhTNode = getTNode(index, viewData);
|
||||||
const removedPhRNode = getNativeByIndex(index, viewData);
|
const removedPhRNode = getNativeByIndex(index, viewData);
|
||||||
if (removedPhRNode) {
|
if (removedPhRNode) {
|
||||||
removeChild(removedPhTNode, removedPhRNode, viewData);
|
removeRNode(removedPhTNode, removedPhRNode, viewData);
|
||||||
}
|
}
|
||||||
|
|
||||||
removedPhTNode.detached = true;
|
removedPhTNode.detached = true;
|
||||||
@ -840,7 +840,7 @@ function removeNode(index: number, viewData: LView) {
|
|||||||
if (isLContainer(slotValue)) {
|
if (isLContainer(slotValue)) {
|
||||||
const lContainer = slotValue as LContainer;
|
const lContainer = slotValue as LContainer;
|
||||||
if (removedPhTNode.type !== TNodeType.Container) {
|
if (removedPhTNode.type !== TNodeType.Container) {
|
||||||
removeChild(removedPhTNode, lContainer[NATIVE], viewData);
|
removeRNode(removedPhTNode, lContainer[NATIVE], viewData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,7 @@ function executeNodeAction(
|
|||||||
if (action === WalkTNodeTreeAction.Insert) {
|
if (action === WalkTNodeTreeAction.Insert) {
|
||||||
nativeInsertBefore(renderer, parent !, node, beforeNode || null);
|
nativeInsertBefore(renderer, parent !, node, beforeNode || null);
|
||||||
} else if (action === WalkTNodeTreeAction.Detach) {
|
} else if (action === WalkTNodeTreeAction.Detach) {
|
||||||
nativeRemoveChild(renderer, parent !, node, isComponent(tNode));
|
nativeRemoveChild(renderer, node, isComponent(tNode));
|
||||||
} else if (action === WalkTNodeTreeAction.Destroy) {
|
} else if (action === WalkTNodeTreeAction.Destroy) {
|
||||||
ngDevMode && ngDevMode.rendererDestroyNode++;
|
ngDevMode && ngDevMode.rendererDestroyNode++;
|
||||||
(renderer as ProceduralRenderer3).destroyNode !(node);
|
(renderer as ProceduralRenderer3).destroyNode !(node);
|
||||||
@ -593,13 +593,19 @@ function nativeAppendOrInsertBefore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Removes a node from the DOM. */
|
||||||
* Removes a native child node from a given native parent node.
|
|
||||||
*/
|
|
||||||
export function nativeRemoveChild(
|
export function nativeRemoveChild(
|
||||||
renderer: Renderer3, parent: RElement, child: RNode, isHostElement?: boolean): void {
|
renderer: Renderer3, child: RNode, isHostElement?: boolean): void {
|
||||||
isProceduralRenderer(renderer) ? renderer.removeChild(parent as RElement, child, isHostElement) :
|
if (isProceduralRenderer(renderer)) {
|
||||||
parent.removeChild(child);
|
const renderParent = renderer.parentNode(child);
|
||||||
|
if (renderParent) {
|
||||||
|
renderer.removeChild(renderParent, child, isHostElement);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We intentionally don't use the given parent node since it may no longer
|
||||||
|
// match the state of the DOM (if the child node has been manually moved).
|
||||||
|
child.parentNode && child.parentNode.removeChild(child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -692,14 +698,9 @@ export function getBeforeNodeForView(index: number, views: LView[], containerNat
|
|||||||
* @param childTNode The TNode of the child to remove
|
* @param childTNode The TNode of the child to remove
|
||||||
* @param childEl The child that should be removed
|
* @param childEl The child that should be removed
|
||||||
* @param currentView The current LView
|
* @param currentView The current LView
|
||||||
* @returns Whether or not the child was removed
|
|
||||||
*/
|
*/
|
||||||
export function removeChild(childTNode: TNode, childEl: RNode, currentView: LView): void {
|
export function removeNode(childTNode: TNode, childEl: RNode, currentView: LView): void {
|
||||||
const parentNative = getRenderParent(childTNode, currentView);
|
nativeRemoveChild(currentView[RENDERER], childEl);
|
||||||
// We only remove the element if it already has a render parent.
|
|
||||||
if (parentNative) {
|
|
||||||
nativeRemoveChild(currentView[RENDERER], parentNative, childEl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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';
|
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';
|
||||||
import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing';
|
import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing';
|
||||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||||
@ -1021,6 +1021,58 @@ class TestComp {
|
|||||||
expect(impurePipe4).not.toBe(impurePipe1);
|
expect(impurePipe4).not.toBe(impurePipe1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('view destruction', () => {
|
||||||
|
@Component({selector: 'some-component', template: ''})
|
||||||
|
class SomeComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [SomeComponent],
|
||||||
|
exports: [SomeComponent],
|
||||||
|
entryComponents: [SomeComponent]
|
||||||
|
})
|
||||||
|
class SomeModule {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'listener-and-on-destroy', template: ''})
|
||||||
|
class ComponentThatLoadsAnotherComponentThenMovesIt {
|
||||||
|
constructor(
|
||||||
|
private viewContainerRef: ViewContainerRef,
|
||||||
|
private componentFactoryResolver: ComponentFactoryResolver) {}
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Dynamically load some component.
|
||||||
|
const componentFactory =
|
||||||
|
this.componentFactoryResolver.resolveComponentFactory(SomeComponent);
|
||||||
|
const componentRef =
|
||||||
|
this.viewContainerRef.createComponent(componentFactory, this.viewContainerRef.length);
|
||||||
|
|
||||||
|
// Manually move the loaded component to some arbitrary DOM node.
|
||||||
|
const componentRootNode =
|
||||||
|
(componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
|
||||||
|
document.createElement('div').appendChild(componentRootNode);
|
||||||
|
|
||||||
|
// Destroy the component we just moved to ensure that it does not error during
|
||||||
|
// destruction.
|
||||||
|
componentRef.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should not error when destroying a component that has been moved in the DOM', () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [SomeModule],
|
||||||
|
declarations: [ComponentThatLoadsAnotherComponentThenMovesIt],
|
||||||
|
});
|
||||||
|
const fixture =
|
||||||
|
createComponentFixture(`<listener-and-on-destroy></listener-and-on-destroy>`);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// This test will fail if the ngOnInit of ComponentThatLoadsAnotherComponentThenMovesIt
|
||||||
|
// throws an error.
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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';
|
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';
|
||||||
|
import {createInjector} from '../../src/di/r3_injector';
|
||||||
import {ViewEncapsulation} from '../../src/metadata';
|
import {ViewEncapsulation} from '../../src/metadata';
|
||||||
|
|
||||||
import {AttributeMarker, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, listener, loadViewQuery, NgOnChangesFeature, queryRefresh, viewQuery,} from '../../src/render3/index';
|
import {AttributeMarker, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, listener, loadViewQuery, NgOnChangesFeature, queryRefresh, viewQuery,} from '../../src/render3/index';
|
||||||
|
|
||||||
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';
|
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';
|
|||||||
import {pipe, pipeBind1} from '../../src/render3/pipe';
|
import {pipe, pipeBind1} from '../../src/render3/pipe';
|
||||||
import {getLView} from '../../src/render3/state';
|
import {getLView} from '../../src/render3/state';
|
||||||
import {getNativeByIndex} from '../../src/render3/util';
|
import {getNativeByIndex} from '../../src/render3/util';
|
||||||
import {createInjector} from '../../src/di/r3_injector';
|
|
||||||
import {templateRefExtractor} from '../../src/render3/view_engine_compatibility_prebound';
|
import {templateRefExtractor} from '../../src/render3/view_engine_compatibility_prebound';
|
||||||
import {NgForOf} from '../../test/render3/common_with_def';
|
import {NgForOf} from '../../test/render3/common_with_def';
|
||||||
|
|
||||||
|
@ -87,6 +87,17 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('removeChild', () => {
|
||||||
|
it('should not error when removing a child with a different parent than given', () => {
|
||||||
|
const savedParent = document.createElement('div');
|
||||||
|
const realParent = document.createElement('div');
|
||||||
|
const child = document.createElement('div');
|
||||||
|
|
||||||
|
realParent.appendChild(child);
|
||||||
|
renderer.removeChild(savedParent, child);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (browserDetection.supportsDeprecatedShadowDomV0) {
|
if (browserDetection.supportsDeprecatedShadowDomV0) {
|
||||||
it('should allow to style components with emulated encapsulation and no encapsulation inside of components with shadow DOM',
|
it('should allow to style components with emulated encapsulation and no encapsulation inside of components with shadow DOM',
|
||||||
() => {
|
() => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user