fix(ivy): incorrect namespace for root node created through ViewContainerRef (#31232)

Currently in Ivy whenever we encounter a new namespace, we set it in the global state so that all subsequent nodes are created under the same namespace. Next time a template is run the namespace will be reset back to HTML.

This breaks down if the last node that was rendered was under the SVG or MathML namespace and we create a component through `ViewContainerRef.create`, because the next template function hasn't run yet and it hasn't had the chance to update the namespace. The result is that the root node of the new component will retain the wrong namespace and may not end up rendering at all (e.g. if we're trying to show a `div` inside the SVG namespace). This issue has the potential to affect a lot of apps, because all components inserted through the router also go through `ViewContainerRef.create`.

PR Close #31232
This commit is contained in:
Kristiyan Kostadinov 2019-06-26 09:21:57 +02:00 committed by Alex Rickabaugh
parent d7b4172678
commit f2360aab9d
6 changed files with 59 additions and 2 deletions

View File

@ -30,7 +30,7 @@ import {ComponentDef} from './interfaces/definition';
import {TContainerNode, TElementContainerNode, TElementNode} from './interfaces/node';
import {RNode, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {LView, LViewFlags, RootContext, TVIEW} from './interfaces/view';
import {enterView, leaveView} from './state';
import {enterView, leaveView, namespaceHTMLInternal} from './state';
import {defaultScheduler} from './util/misc_utils';
import {getTNode} from './util/view_utils';
import {createElementRef} from './view_engine_compatibility';
@ -140,6 +140,9 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
rootViewInjector.get(RendererFactory2, domRendererFactory3) as RendererFactory3;
const sanitizer = rootViewInjector.get(Sanitizer, null);
// Ensure that the namespace for the root node is correct,
// otherwise the browser might not render out the element properly.
namespaceHTMLInternal();
const hostRNode = isInternalRootView ?
elementCreate(this.selector, rendererFactory.createRenderer(null, this.componentDef)) :
locateHostElement(rendererFactory, rootSelectorOrNode);

View File

@ -537,12 +537,20 @@ export function ɵɵnamespaceMathML() {
}
/**
* Sets the namespace used to create elements no `null`, which forces element creation to use
* Sets the namespace used to create elements to `null`, which forces element creation to use
* `createElement` rather than `createElementNS`.
*
* @codeGenApi
*/
export function ɵɵnamespaceHTML() {
namespaceHTMLInternal();
}
/**
* Sets the namespace used to create elements to `null`, which forces element creation to use
* `createElement` rather than `createElementNS`.
*/
export function namespaceHTMLInternal() {
_currentNamespace = null;
}

View File

@ -1049,6 +1049,43 @@ describe('ViewContainerRef', () => {
expect(() => fixture.componentRef.destroy()).not.toThrow();
});
it('should create the root node in the correct namespace when previous node is SVG', () => {
@Component({
template: `
<div>Some random content</div>
<!-- Note that it's important for the test that the <svg> element is last. -->
<svg></svg>
`
})
class TestComp {
constructor(
public viewContainerRef: ViewContainerRef,
public componentFactoryResolver: ComponentFactoryResolver) {}
}
@Component({selector: 'dynamic-comp', template: ''})
class DynamicComponent {
}
@NgModule({declarations: [DynamicComponent], entryComponents: [DynamicComponent]})
class DeclaresDynamicComponent {
}
TestBed.configureTestingModule(
{imports: [DeclaresDynamicComponent], declarations: [TestComp]});
const fixture = TestBed.createComponent(TestComp);
// Note: it's important that we **don't** call `fixture.detectChanges` between here and
// the component being created, because running change detection will reset Ivy's
// namespace state which will make the test pass.
const {viewContainerRef, componentFactoryResolver} = fixture.componentInstance;
const componentRef = viewContainerRef.createComponent(
componentFactoryResolver.resolveComponentFactory(DynamicComponent));
const element = componentRef.location.nativeElement;
expect((element.namespaceURI || '').toLowerCase()).not.toContain('svg');
});
});
describe('insertion points and declaration points', () => {

View File

@ -581,6 +581,9 @@
{
"name": "matchTemplateAttribute"
},
{
"name": "namespaceHTMLInternal"
},
{
"name": "nativeAppendChild"
},

View File

@ -386,6 +386,9 @@
{
"name": "locateHostElement"
},
{
"name": "namespaceHTMLInternal"
},
{
"name": "nativeAppendChild"
},

View File

@ -1244,6 +1244,9 @@
{
"name": "matchTemplateAttribute"
},
{
"name": "namespaceHTMLInternal"
},
{
"name": "nativeAppendChild"
},