feat(ivy): now supports SVG and MathML elements (#24377)

- Adds support for ivy creating SVG and MathML elements properly using
createElementNS

PR Close #24377
This commit is contained in:
Ben Lesh 2018-06-08 09:00:01 -07:00 committed by Miško Hevery
parent 5ef7a07c4b
commit 8c1ac28275
11 changed files with 273 additions and 7 deletions

View File

@ -17,6 +17,12 @@ export class Identifiers {
static PATCH_DEPS = 'patchedDeps'; static PATCH_DEPS = 'patchedDeps';
/* Instructions */ /* Instructions */
static namespaceHTML: o.ExternalReference = {name: 'ɵNH', moduleName: CORE};
static namespaceMathML: o.ExternalReference = {name: 'ɵNM', moduleName: CORE};
static namespaceSVG: o.ExternalReference = {name: 'ɵNS', moduleName: CORE};
static createElement: o.ExternalReference = {name: 'ɵE', moduleName: CORE}; static createElement: o.ExternalReference = {name: 'ɵE', moduleName: CORE};
static elementEnd: o.ExternalReference = {name: 'ɵe', moduleName: CORE}; static elementEnd: o.ExternalReference = {name: 'ɵe', moduleName: CORE};

View File

@ -136,7 +136,8 @@ export function compileComponentFromMetadata(
const templateFunctionExpression = const templateFunctionExpression =
new TemplateDefinitionBuilder( new TemplateDefinitionBuilder(
constantPool, CONTEXT_NAME, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName, constantPool, CONTEXT_NAME, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName,
meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed) meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed,
R3.namespaceHTML)
.buildTemplateFunction( .buildTemplateFunction(
template.nodes, [], template.hasNgContent, template.ngContentSelectors); template.nodes, [], template.hasNgContent, template.ngContentSelectors);

View File

@ -18,6 +18,7 @@ import * as html from '../../ml_parser/ast';
import {HtmlParser} from '../../ml_parser/html_parser'; import {HtmlParser} from '../../ml_parser/html_parser';
import {WhitespaceVisitor} from '../../ml_parser/html_whitespaces'; import {WhitespaceVisitor} from '../../ml_parser/html_whitespaces';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
import {splitNsName} from '../../ml_parser/tags';
import * as o from '../../output/output_ast'; import * as o from '../../output/output_ast';
import {ParseError, ParseSourceSpan} from '../../parse_util'; import {ParseError, ParseSourceSpan} from '../../parse_util';
import {DomElementSchemaRegistry} from '../../schema/dom_element_schema_registry'; import {DomElementSchemaRegistry} from '../../schema/dom_element_schema_registry';
@ -66,7 +67,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
parentBindingScope: BindingScope, private level = 0, private contextName: string|null, parentBindingScope: BindingScope, private level = 0, private contextName: string|null,
private templateName: string|null, private viewQueries: R3QueryMetadata[], private templateName: string|null, private viewQueries: R3QueryMetadata[],
private directiveMatcher: SelectorMatcher|null, private directives: Set<o.Expression>, private directiveMatcher: SelectorMatcher|null, private directives: Set<o.Expression>,
private pipeTypeByName: Map<string, o.Expression>, private pipes: Set<o.Expression>) { private pipeTypeByName: Map<string, o.Expression>, private pipes: Set<o.Expression>,
private _namespace: o.ExternalReference) {
this._bindingScope = this._bindingScope =
parentBindingScope.nestedScope((lhsVar: o.ReadVarExpr, expression: o.Expression) => { parentBindingScope.nestedScope((lhsVar: o.ReadVarExpr, expression: o.Expression) => {
this._bindingCode.push( this._bindingCode.push(
@ -89,6 +91,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
buildTemplateFunction( buildTemplateFunction(
nodes: t.Node[], variables: t.Variable[], hasNgContent: boolean = false, nodes: t.Node[], variables: t.Variable[], hasNgContent: boolean = false,
ngContentSelectors: string[] = []): o.FunctionExpr { ngContentSelectors: string[] = []): o.FunctionExpr {
if (this._namespace !== R3.namespaceHTML) {
this.instruction(this._creationCode, null, this._namespace);
}
// Create variable bindings // Create variable bindings
for (const variable of variables) { for (const variable of variables) {
const variableName = variable.name; const variableName = variable.name;
@ -220,6 +226,23 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.instruction(this._creationCode, ngContent.sourceSpan, R3.projection, ...parameters); this.instruction(this._creationCode, ngContent.sourceSpan, R3.projection, ...parameters);
} }
getNamespaceInstruction(namespaceKey: string|null) {
switch (namespaceKey) {
case 'math':
return R3.namespaceMathML;
case 'svg':
return R3.namespaceSVG;
default:
return R3.namespaceHTML;
}
}
addNamespaceInstruction(nsInstruction: o.ExternalReference, element: t.Element) {
this._namespace = nsInstruction;
this.instruction(this._creationCode, element.sourceSpan, nsInstruction);
}
visitElement(element: t.Element) { visitElement(element: t.Element) {
const elementIndex = this.allocateDataSlot(); const elementIndex = this.allocateDataSlot();
const referenceDataSlots = new Map<string, number>(); const referenceDataSlots = new Map<string, number>();
@ -229,6 +252,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const attrI18nMetas: {[name: string]: string} = {}; const attrI18nMetas: {[name: string]: string} = {};
let i18nMeta: string = ''; let i18nMeta: string = '';
const [namespaceKey, elementName] = splitNsName(element.name);
// Elements inside i18n sections are replaced with placeholders // Elements inside i18n sections are replaced with placeholders
// TODO(vicb): nested elements are a WIP in this phase // TODO(vicb): nested elements are a WIP in this phase
if (this._inI18nSection) { if (this._inI18nSection) {
@ -269,7 +294,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Element creation mode // Element creation mode
const parameters: o.Expression[] = [ const parameters: o.Expression[] = [
o.literal(elementIndex), o.literal(elementIndex),
o.literal(element.name), o.literal(elementName),
]; ];
// Add the attributes // Add the attributes
@ -314,6 +339,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (i18nMessages.length > 0) { if (i18nMessages.length > 0) {
this._creationCode.push(...i18nMessages); this._creationCode.push(...i18nMessages);
} }
const wasInNamespace = this._namespace;
const currentNamespace = this.getNamespaceInstruction(namespaceKey);
// If the namespace is changing now, include an instruction to change it
// during element creation.
if (currentNamespace !== wasInNamespace) {
this.addNamespaceInstruction(currentNamespace, element);
}
this.instruction( this.instruction(
this._creationCode, element.sourceSpan, R3.createElement, ...trimTrailingNulls(parameters)); this._creationCode, element.sourceSpan, R3.createElement, ...trimTrailingNulls(parameters));
@ -433,7 +468,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Create the template function // Create the template function
const templateVisitor = new TemplateDefinitionBuilder( const templateVisitor = new TemplateDefinitionBuilder(
this.constantPool, templateContext, this._bindingScope, this.level + 1, contextName, this.constantPool, templateContext, this._bindingScope, this.level + 1, contextName,
templateName, [], this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes); templateName, [], this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes,
this._namespace);
const templateFunctionExpr = const templateFunctionExpr =
templateVisitor.buildTemplateFunction(template.children, template.variables); templateVisitor.buildTemplateFunction(template.children, template.variables);
this._postfixCode.push(templateFunctionExpr.toDeclStmt(templateName, null)); this._postfixCode.push(templateFunctionExpr.toDeclStmt(templateName, null));

View File

@ -21,6 +21,105 @@ describe('compiler compliance', () => {
}); });
describe('elements', () => { describe('elements', () => {
it('should handle SVG', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div class="my-app" title="Hello"><svg><circle cx="20" cy="30" r="50"/></svg><p>test</p></div>\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
// The factory should look like this:
const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
// The template should look like this (where IDENT is a wild card for an identifier):
const template = `
const $c1$ = ['class', 'my-app', 'title', 'Hello'];
const $c2$ = ['cx', '20', 'cy', '30', 'r', '50'];
template: function MyComponent_Template(rf: IDENT, ctx: IDENT) {
if (rf & 1) {
$r3$.ɵE(0, 'div', $c1$);
$r3$.ɵNS();
$r3$.ɵE(1, 'svg');
$r3$.ɵE(2, 'circle', $c2$);
$r3$.ɵe();
$r3$.ɵe();
$r3$.ɵNH();
$r3$.ɵE(3, 'p');
$r3$.ɵT(4, 'test');
$r3$.ɵe();
$r3$.ɵe();
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, factory, 'Incorrect factory');
expectEmit(result.source, template, 'Incorrect template');
});
it('should handle MathML', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div class="my-app" title="Hello"><math><infinity/></math><p>test</p></div>\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
// The factory should look like this:
const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
// The template should look like this (where IDENT is a wild card for an identifier):
const template = `
const $c1$ = ['class', 'my-app', 'title', 'Hello'];
template: function MyComponent_Template(rf: IDENT, ctx: IDENT) {
if (rf & 1) {
$r3$.ɵE(0, 'div', $c1$);
$r3$.ɵNM();
$r3$.ɵE(1, 'math');
$r3$.ɵE(2, 'infinity');
$r3$.ɵe();
$r3$.ɵe();
$r3$.ɵNH();
$r3$.ɵE(3, 'p');
$r3$.ɵT(4, 'test');
$r3$.ɵe();
$r3$.ɵe();
}
}
`;
const result = compile(files, angularFiles);
expectEmit(result.source, factory, 'Incorrect factory');
expectEmit(result.source, template, 'Incorrect template');
});
it('should translate DOM structure', () => { it('should translate DOM structure', () => {
const files = { const files = {
app: { app: {
@ -1164,6 +1263,80 @@ describe('compiler compliance', () => {
} }
}; };
it('should support embedded views in the SVG namespace', () => {
const files = {
app: {
...shared,
'spec.ts': `
import {Component, NgModule} from '@angular/core';
import {ForOfDirective} from './shared/for_of';
@Component({
selector: 'my-component',
template: \`<svg><g *for="let item of items"><circle></circle></g></svg>\`
})
export class MyComponent {
items = [{ data: 42 }, { data: 42 }];
}
@NgModule({
declarations: [MyComponent, ForOfDirective]
})
export class MyModule {}
`
}
};
// TODO(benlesh): Enforce this when the directives are specified
const ForDirectiveDefinition = `
static ngDirectiveDef = $r3$.ɵdefineDirective({
type: ForOfDirective,
selectors: [['', 'forOf', '']],
factory: function ForOfDirective_Factory() {
return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef());
},
features: [$r3$.ɵNgOnChangesFeature(NgForOf)],
inputs: {forOf: 'forOf'}
});
`;
const MyComponentDefinition = `
const $_c0$ = ['for','','forOf',''];
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyComponent,
selectors: [['my-component']],
factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(rf:IDENT,ctx:IDENT){
if (rf & 1) {
$r3$.ɵNS();
$r3$.ɵE(0,'svg');
$r3$.ɵC(1,MyComponent__svg_g_Template_1,null,$_c0$);
$r3$.ɵe();
}
if (rf & 2) { $r3$.ɵp(1,'forOf',$r3$.ɵb(ctx.items)); }
function MyComponent__svg_g_Template_1(rf:IDENT,ctx0:IDENT) {
if (rf & 1) {
$r3$.ɵNS();
$r3$.ɵE(0,'g');
$r3$.ɵE(1,'circle');
$r3$.ɵe();
$r3$.ɵe();
}
}
},
directives: [ForOfDirective]
});
`;
const result = compile(files, angularFiles);
const source = result.source;
// TODO(benlesh): Enforce this when the directives are specified
// expectEmit(source, ForDirectiveDefinition, 'Invalid directive definition');
expectEmit(source, MyComponentDefinition, 'Invalid component definition');
});
it('should support a let variable and reference', () => { it('should support a let variable and reference', () => {
const files = { const files = {
app: { app: {

View File

@ -30,6 +30,9 @@ export {
NC as ɵNC, NC as ɵNC,
C as ɵC, C as ɵC,
E as ɵE, E as ɵE,
NH as ɵNH,
NM as ɵNM,
NS as ɵNS,
L as ɵL, L as ɵL,
T as ɵT, T as ɵT,
V as ɵV, V as ɵV,

View File

@ -150,7 +150,7 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S
| `{{ ['literal', exp ] }}` | ✅ | ✅ | ✅ | | `{{ ['literal', exp ] }}` | ✅ | ✅ | ✅ |
| `{{ { a: 'literal', b: exp } }}` | ✅ | ✅ | ✅ | | `{{ { a: 'literal', b: exp } }}` | ✅ | ✅ | ✅ |
| `{{ exp \| pipe: arg }}` | ✅ | ✅ | ✅ | | `{{ exp \| pipe: arg }}` | ✅ | ✅ | ✅ |
| `<svg:g svg:p>` | ❌ | ❌ | ❌ | | `<svg:g svg:p>` | ✅ | ✅ | ✅ |
| `<img src=[userData]>` sanitization | ❌ | ❌ | ❌ | | `<img src=[userData]>` sanitization | ❌ | ❌ | ❌ |
| `<div (nocd.click)>` | ❌ | ❌ | ❌ | | `<div (nocd.click)>` | ❌ | ❌ | ❌ |
| `<div (bubble.click)>` | ❌ | ❌ | ❌ | | `<div (bubble.click)>` | ❌ | ❌ | ❌ |

View File

@ -58,6 +58,10 @@ export {
load as ld, load as ld,
loadDirective as d, loadDirective as d,
namespaceHTML as NH,
namespaceMathML as NM,
namespaceSVG as NS,
projection as P, projection as P,
projectionDef as pD, projectionDef as pD,

View File

@ -495,6 +495,7 @@ export function renderEmbeddedTemplate<T>(
rf = RenderFlags.Create; rf = RenderFlags.Create;
} }
oldView = enterView(viewNode.data, viewNode); oldView = enterView(viewNode.data, viewNode);
namespaceHTML();
tView.template !(rf, context); tView.template !(rf, context);
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
refreshView(); refreshView();
@ -520,6 +521,7 @@ export function renderComponentOrTemplate<T>(
rendererFactory.begin(); rendererFactory.begin();
} }
if (template) { if (template) {
namespaceHTML();
template(getRenderFlags(hostView), componentOrContext !); template(getRenderFlags(hostView), componentOrContext !);
refreshView(); refreshView();
} else { } else {
@ -552,6 +554,24 @@ function getRenderFlags(view: LView): RenderFlags {
RenderFlags.Update; RenderFlags.Update;
} }
//////////////////////////
//// Namespace
//////////////////////////
let _currentNamespace: string|null = null;
export function namespaceSVG() {
_currentNamespace = 'http://www.w3.org/2000/svg/';
}
export function namespaceMathML() {
_currentNamespace = 'http://www.w3.org/1998/MathML/';
}
export function namespaceHTML() {
_currentNamespace = null;
}
////////////////////////// //////////////////////////
//// Element //// Element
////////////////////////// //////////////////////////
@ -575,7 +595,19 @@ export function elementStart(
assertEqual(currentView.bindingIndex, -1, 'elements should be created before any bindings'); assertEqual(currentView.bindingIndex, -1, 'elements should be created before any bindings');
ngDevMode && ngDevMode.rendererCreateElement++; ngDevMode && ngDevMode.rendererCreateElement++;
const native: RElement = renderer.createElement(name);
let native: RElement;
if (isProceduralRenderer(renderer)) {
native = renderer.createElement(name, _currentNamespace);
} else {
if (_currentNamespace === null) {
native = renderer.createElement(name);
} else {
native = renderer.createElementNS(_currentNamespace, name);
}
}
ngDevMode && assertDataInRange(index - 1); ngDevMode && assertDataInRange(index - 1);
const node: LElementNode = const node: LElementNode =
@ -2130,6 +2162,7 @@ export function detectChangesInternal<T>(hostView: LView, hostNode: LElementNode
const template = hostView.tView.template !; const template = hostView.tView.template !;
try { try {
namespaceHTML();
template(getRenderFlags(hostView), component); template(getRenderFlags(hostView), component);
refreshView(); refreshView();
} finally { } finally {

View File

@ -36,6 +36,7 @@ export type Renderer3 = ObjectOrientedRenderer3 | ProceduralRenderer3;
* */ * */
export interface ObjectOrientedRenderer3 { export interface ObjectOrientedRenderer3 {
createElement(tagName: string): RElement; createElement(tagName: string): RElement;
createElementNS(namespace: string, tagName: string): RElement;
createTextNode(data: string): RText; createTextNode(data: string): RText;
querySelector(selectors: string): RElement|null; querySelector(selectors: string): RElement|null;

View File

@ -36,6 +36,9 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵcR': r3.cR, 'ɵcR': r3.cR,
'ɵcr': r3.cr, 'ɵcr': r3.cr,
'ɵd': r3.d, 'ɵd': r3.d,
'ɵNH': r3.NH,
'ɵNM': r3.NM,
'ɵNS': r3.NS,
'ɵE': r3.E, 'ɵE': r3.E,
'ɵe': r3.e, 'ɵe': r3.e,
'ɵf0': r3.f0, 'ɵf0': r3.f0,

View File

@ -176,6 +176,9 @@
{ {
"name": "_currentInjector" "name": "_currentInjector"
}, },
{
"name": "_currentNamespace"
},
{ {
"name": "_devMode" "name": "_devMode"
}, },
@ -545,6 +548,9 @@
{ {
"name": "markViewDirty" "name": "markViewDirty"
}, },
{
"name": "namespaceHTML"
},
{ {
"name": "notImplemented" "name": "notImplemented"
}, },