Permalink
Browse files

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

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

PR Close #24377
  • Loading branch information...
benlesh authored and mhevery committed Jun 8, 2018
1 parent 5ef7a07 commit 8c1ac282759035c1134bce92260faec14b959ad0
@@ -17,6 +17,12 @@ export class Identifiers {
static PATCH_DEPS = 'patchedDeps';
/* 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 elementEnd: o.ExternalReference = {name: 'ɵe', moduleName: CORE};
@@ -136,7 +136,8 @@ export function compileComponentFromMetadata(
const templateFunctionExpression =
new TemplateDefinitionBuilder(
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(
template.nodes, [], template.hasNgContent, template.ngContentSelectors);
@@ -443,4 +444,4 @@ function typeMapToExpressionMap(
const entries = Array.from(map).map(
([key, type]): [string, o.Expression] => [key, outputCtx.importExpr(type)]);
return new Map(entries);
}
}
@@ -18,6 +18,7 @@ import * as html from '../../ml_parser/ast';
import {HtmlParser} from '../../ml_parser/html_parser';
import {WhitespaceVisitor} from '../../ml_parser/html_whitespaces';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
import {splitNsName} from '../../ml_parser/tags';
import * as o from '../../output/output_ast';
import {ParseError, ParseSourceSpan} from '../../parse_util';
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,
private templateName: string|null, private viewQueries: R3QueryMetadata[],
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 =
parentBindingScope.nestedScope((lhsVar: o.ReadVarExpr, expression: o.Expression) => {
this._bindingCode.push(
@@ -89,6 +91,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
buildTemplateFunction(
nodes: t.Node[], variables: t.Variable[], hasNgContent: boolean = false,
ngContentSelectors: string[] = []): o.FunctionExpr {
if (this._namespace !== R3.namespaceHTML) {
this.instruction(this._creationCode, null, this._namespace);
}
// Create variable bindings
for (const variable of variables) {
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);
}
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) {
const elementIndex = this.allocateDataSlot();
const referenceDataSlots = new Map<string, number>();
@@ -229,6 +252,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const attrI18nMetas: {[name: string]: string} = {};
let i18nMeta: string = '';
const [namespaceKey, elementName] = splitNsName(element.name);
// Elements inside i18n sections are replaced with placeholders
// TODO(vicb): nested elements are a WIP in this phase
if (this._inI18nSection) {
@@ -269,7 +294,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Element creation mode
const parameters: o.Expression[] = [
o.literal(elementIndex),
o.literal(element.name),
o.literal(elementName),
];
// Add the attributes
@@ -314,6 +339,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (i18nMessages.length > 0) {
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._creationCode, element.sourceSpan, R3.createElement, ...trimTrailingNulls(parameters));
@@ -433,7 +468,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Create the template function
const templateVisitor = new TemplateDefinitionBuilder(
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 =
templateVisitor.buildTemplateFunction(template.children, template.variables);
this._postfixCode.push(templateFunctionExpr.toDeclStmt(templateName, null));
@@ -21,6 +21,105 @@ describe('compiler compliance', () => {
});
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', () => {
const files = {
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', () => {
const files = {
app: {
@@ -30,6 +30,9 @@ export {
NC as ɵNC,
C as ɵC,
E as ɵE,
NH as ɵNH,
NM as ɵNM,
NS as ɵNS,
L as ɵL,
T as ɵT,
V as ɵV,
@@ -150,7 +150,7 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S
| `{{ ['literal', exp ] }}` | ✅ | ✅ | ✅ |
| `{{ { a: 'literal', b: exp } }}` | ✅ | ✅ | ✅ |
| `{{ exp \| pipe: arg }}` | ✅ | ✅ | ✅ |
| `<svg:g svg:p>` | | | |
| `<svg:g svg:p>` | | | |
| `<img src=[userData]>` sanitization | ❌ | ❌ | ❌ |
| `<div (nocd.click)>` | ❌ | ❌ | ❌ |
| `<div (bubble.click)>` | ❌ | ❌ | ❌ |
@@ -58,6 +58,10 @@ export {
load as ld,
loadDirective as d,
namespaceHTML as NH,
namespaceMathML as NM,
namespaceSVG as NS,
projection as P,
projectionDef as pD,
Oops, something went wrong.

0 comments on commit 8c1ac28

Please sign in to comment.