From 1007d1ad271566a8beabb2fcf234be393b3ce313 Mon Sep 17 00:00:00 2001 From: Ben Lesh Date: Fri, 1 Jun 2018 08:02:14 -0700 Subject: [PATCH] feat(ivy): SVG now handled by ivy compiler (#23899) PR Close #23899 --- .../compiler/src/render3/r3_identifiers.ts | 6 + .../compiler/src/render3/view/template.ts | 42 ++++++- .../render3/r3_compiler_compliance_spec.ts | 116 ++++++++++++++++++ .../core/src/core_render3_private_export.ts | 4 + packages/core/src/render3/index.ts | 5 + packages/core/src/render3/instructions.ts | 3 + .../bundling/todo/bundle.golden_symbols.json | 3 + .../compiler_canonical/elements_spec.ts | 38 ++++++ .../template_variables_spec.ts | 51 ++++++++ 9 files changed, 267 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index a37e2a7bdc..57e4b5fb91 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -39,6 +39,12 @@ export class Identifiers { static bind: o.ExternalReference = {name: 'ɵb', moduleName: CORE}; + + static namespace: o.ExternalReference = {name: 'ɵN', moduleName: CORE}; + 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 interpolation1: o.ExternalReference = {name: 'ɵi1', moduleName: CORE}; static interpolation2: o.ExternalReference = {name: 'ɵi2', moduleName: CORE}; static interpolation3: o.ExternalReference = {name: 'ɵi3', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 49e593ba90..9d7a7163d4 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -19,6 +19,7 @@ import {HtmlParser} from '../../ml_parser/html_parser'; import {WhitespaceVisitor} from '../../ml_parser/html_whitespaces'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config'; import * as o from '../../output/output_ast'; +import {ExternalReference} from '../../output/output_ast'; import {ParseError, ParseSourceSpan} from '../../parse_util'; import {DomElementSchemaRegistry} from '../../schema/dom_element_schema_registry'; import {CssSelector, SelectorMatcher} from '../../selector'; @@ -51,6 +52,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private _valueConverter: ValueConverter; private _unsupported = unsupported; private _bindingScope: BindingScope; + private _namespace = R3.namespaceHTML; // Whether we are inside a translatable element (`

... somewhere here ...

) private _inI18nSection: boolean = false; @@ -220,6 +222,26 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this.instruction(this._creationCode, ngContent.sourceSpan, R3.projection, ...parameters); } + /** + * Gets the namespace instruction function based on the current element + * @param ivyElementName An system element name, can include colons like :svg:svg + */ + getNamespaceInstruction(ivyElementName: string) { + switch (ivyElementName) { + case ':svg:svg': + return R3.namespaceSVG; + case ':math:math': + return R3.namespaceMathML; + default: + return this._namespace; + } + } + + addNamespaceInstruction(nsInstruction: 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(); @@ -315,11 +337,20 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this._creationCode.push(...i18nMessages); } - const isEmptyElement = element.outputs.length === 0 && element.children.length === 0; + const isEmptyElement = element.children.length === 0 && element.outputs.length === 0; const implicit = o.variable(CONTEXT_NAME); + const wasInNamespace = this._namespace; + const currentNamespace = this.getNamespaceInstruction(element.name); + // If the namespace is changing now, include an instruction to change it + // during element creation. + if (currentNamespace !== wasInNamespace) { + this.addNamespaceInstruction(currentNamespace, element); + } + + if (isEmptyElement) { this.instruction( this._creationCode, element.sourceSpan, R3.element, ...trimTrailingNulls(parameters)); @@ -328,6 +359,14 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this._creationCode, element.sourceSpan, R3.elementStart, ...trimTrailingNulls(parameters)); + // If the element happens to be an SVG , we need to switch + // to the HTML namespace inside of it + if (element.name === ':svg:foreignObject') { + // NOTE(benlesh): this may cause extremem corner-case bugs if someone was to do something + // like ....... + this.addNamespaceInstruction(R3.namespaceHTML, element); + } + // Generate Listeners (outputs) element.outputs.forEach((outputAst: t.BoundEvent) => { const elName = sanitizeIdentifier(element.name); @@ -384,6 +423,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } // Restore the state before exiting this node this._inI18nSection = wasInI18nSection; + this._namespace = wasInNamespace; } visitTemplate(template: t.Template) { diff --git a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts index 74045c062b..322af1a50c 100644 --- a/packages/compiler/test/render3/r3_compiler_compliance_spec.ts +++ b/packages/compiler/test/render3/r3_compiler_compliance_spec.ts @@ -66,6 +66,50 @@ describe('compiler compliance', () => { expectEmit(result.source, template, 'Incorrect template'); }); + it('should translate DOM structure for SVG', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`
\` + }) + 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', $e0_attrs$); + $r3$.ɵNS(); + $r3$.ɵE(1, ':svg:svg'); + $r3$.ɵEe(2, ':svg:circle', $e2_attrs$); + $r3$.ɵe(); + $r3$.ɵe(); + } + } + `; + + + const result = compile(files, angularFiles); + + expectEmit(result.source, factory, 'Incorrect factory'); + expectEmit(result.source, template, 'Incorrect template'); + }); + it('should bind to element properties', () => { const files = { app: { @@ -1230,6 +1274,78 @@ describe('compiler compliance', () => { expectEmit(source, MyComponentDefinition, 'Invalid component definition'); }); + it('should support a let variable and reference for SVG', () => { + const files = { + app: { + ...shared, + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + import {ForOfDirective} from './shared/for_of'; + + @Component({ + selector: 'my-component', + template: \`\` + }) + export class MyComponent { + items = [{ data: 42 }, { data: 42 }]; + } + + @NgModule({ + declarations: [MyComponent, ForOfDirective] + }) + export class MyModule {} + ` + } + }; + + // TODO(chuckj): 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: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$.ɵE(0,':svg:g'); + $r3$.ɵEe(1,':svg:circle'); + $r3$.ɵe(); + } + } + }, + directives: [ForOfDirective] + }); + `; + + const result = compile(files, angularFiles); + const source = result.source; + + // TODO(chuckj): Enforce this when the directives are specified + // expectEmit(source, ForDirectiveDefinition, 'Invalid directive definition'); + expectEmit(source, MyComponentDefinition, 'Invalid component definition'); + }); + it('should support accessing parent template variables', () => { const files = { app: { diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index edcd9abbf5..0be275e0b1 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -45,6 +45,10 @@ export { i7 as ɵi7, i8 as ɵi8, iV as ɵiV, + N as ɵN, + NH as ɵNH, + NM as ɵNM, + NS as ɵNS, pb1 as ɵpb1, pb2 as ɵpb2, pb3 as ɵpb3, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 19929ac932..fd08cc9a0f 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -54,6 +54,11 @@ export { elementStyle as s, elementStyleNamed as sn, + namespace as N, + namespaceHTML as NH, + namespaceMathML as NM, + namespaceSVG as NS, + listener as L, store as st, load as ld, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index e195770c05..c8efc2cbe3 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -496,6 +496,7 @@ export function renderEmbeddedTemplate( rf = RenderFlags.Create; } oldView = enterView(viewNode.data, viewNode); + namespaceHTML(); tView.template !(rf, context); if (rf & RenderFlags.Update) { refreshView(); @@ -521,6 +522,7 @@ export function renderComponentOrTemplate( rendererFactory.begin(); } if (template) { + namespaceHTML(); template(getRenderFlags(hostView), componentOrContext !); refreshView(); } else { @@ -2186,6 +2188,7 @@ export function detectChangesInternal(hostView: LView, hostNode: LElementNode const template = hostView.tView.template !; try { + namespaceHTML(); template(getRenderFlags(hostView), component); refreshView(); } finally { diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 8a2c2c97c0..79c48eea7a 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -545,6 +545,9 @@ { "name": "markViewDirty" }, + { + "name": "namespaceHTML" + }, { "name": "notImplemented" }, diff --git a/packages/core/test/render3/compiler_canonical/elements_spec.ts b/packages/core/test/render3/compiler_canonical/elements_spec.ts index 1155b8b229..aacdf0a3bf 100644 --- a/packages/core/test/render3/compiler_canonical/elements_spec.ts +++ b/packages/core/test/render3/compiler_canonical/elements_spec.ts @@ -55,6 +55,44 @@ describe('elements', () => { .toEqual('
Hello World!
'); }); + it('should translate DOM structure with SVG', () => { + type $MyComponent$ = MyComponent; + + // Important: keep arrays outside of function to not create new instances. + const $e0_attrs$ = ['class', 'my-app', 'title', 'Hello']; + const $e2_attrs$ = ['cx', '50', 'cy', '100', 'r', '25']; + + @Component({ + selector: 'my-component', + template: + `
` + }) + class MyComponent { + // NORMATIVE + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + selectors: [['my-component']], + factory: () => new MyComponent(), + template: function(rf: $RenderFlags$, ctx: $MyComponent$) { + if (rf & 1) { + $r3$.ɵE(0, 'div', $e0_attrs$); + $r3$.ɵNS(); + $r3$.ɵE(1, 'svg'); + $r3$.ɵEe(2, 'circle', $e2_attrs$); + $r3$.ɵe(); + $r3$.ɵNH(); + $r3$.ɵe(); + } + } + }); + // /NORMATIVE + } + + expect(toHtml(renderComponent(MyComponent))) + .toEqual( + '
'); + }); + it('should support local refs', () => { type $LocalRefComp$ = LocalRefComp; diff --git a/packages/core/test/render3/compiler_canonical/template_variables_spec.ts b/packages/core/test/render3/compiler_canonical/template_variables_spec.ts index 26c198b24c..f3ab25ad1d 100644 --- a/packages/core/test/render3/compiler_canonical/template_variables_spec.ts +++ b/packages/core/test/render3/compiler_canonical/template_variables_spec.ts @@ -133,6 +133,57 @@ describe('template variables', () => { expect(toHtml(renderComponent(MyComponent))).toEqual('
    '); }); + it('should support a let variable and reference inside of SVG', () => { + type $MyComponent$ = MyComponent; + const $_c0$ = ['for', '', 'forOf', '']; + + interface Item { + name: number; + } + + @Component({ + selector: 'my-component', + template: `` + }) + class MyComponent { + items = [{data: 42}, {data: 42}]; + + // NORMATIVE + static ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + selectors: [['my-component']], + factory: function MyComponent_Factory() { return new MyComponent(); }, + template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { + 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: $RenderFlags$, ctx0: $MyComponent$) { + if (rf & 1) { + $r3$.ɵE(0, 'g'); + $r3$.ɵEe(1, 'circle'); + $r3$.ɵe(); + } + } + } + }); + // /NORMATIVE + } + + // NON-NORMATIVE + (MyComponent.ngComponentDef as ComponentDef).directiveDefs = + [ForOfDirective.ngDirectiveDef]; + // /NON-NORMATIVE + + // TODO(chuckj): update when the changes to enable ngForOf lands. + expect(toHtml(renderComponent(MyComponent))).toEqual(''); + }); + it('should support accessing parent template variables', () => { type $MyComponent$ = MyComponent;