diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index d441544140..8f35af395d 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -443,8 +443,7 @@ describe('compiler compliance', () => { $r3$.ɵɵpureFunction2(2, $_c0$, ctx.collapsedHeight, ctx.expandedHeight) ) , null, true - ); - $r3$.ɵɵupdateSyntheticHostBinding("@expansionWidth", + )("@expansionWidth", $r3$.ɵɵpureFunction2(11, $_c1$, ctx.getExpandedState(), $r3$.ɵɵpureFunction2(8, $_c2$, ctx.collapsedWidth, ctx.expandedWidth) ) diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts index f86841dc00..4f1e6b084e 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts @@ -210,7 +210,7 @@ describe('compiler compliance: bindings', () => { expectEmit(result.source, template, 'Incorrect template'); }); - it('should chain property bindings in the presence of other instructions', () => { + it('should chain property bindings in the presence of other bindings', () => { const files = { app: { 'example.ts': ` @@ -473,7 +473,7 @@ describe('compiler compliance: bindings', () => { expectEmit(result.source, template, 'Incorrect template'); }); - it('should chain attribute bindings in the presence of other instructions', () => { + it('should chain attribute bindings in the presence of other bindings', () => { const files = { app: { 'example.ts': ` @@ -860,6 +860,250 @@ describe('compiler compliance: bindings', () => { const source = result.source; expectEmit(source, CompAndDirDeclaration, 'Invalid host attribute code'); }); + + it('should chain multiple host property bindings into a single instruction', () => { + const files = { + app: { + 'example.ts': ` + import {Directive} from '@angular/core'; + + @Directive({ + selector: '[my-dir]', + host: { + '[title]': 'myTitle', + '[tabindex]': '1', + '[id]': 'myId' + } + }) + export class MyDirective { + myTitle = 'hello'; + myId = 'special-directive'; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) { + … + if (rf & 2) { + $r3$.ɵɵproperty("title", ctx.myTitle, null, true)("tabindex", 1, null, true)("id", ctx.myId, null, true); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain both host properties in the decorator and on the class', () => { + const files = { + app: { + 'example.ts': ` + import {Directive, HostBinding} from '@angular/core'; + + @Directive({ + selector: '[my-dir]', + host: { + '[tabindex]': '1' + } + }) + export class MyDirective { + @HostBinding('title') + myTitle = 'hello'; + + @HostBinding('id') + myId = 'special-directive'; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) { + … + if (rf & 2) { + $r3$.ɵɵproperty("tabindex", 1, null, true)("title", ctx.myTitle, null, true)("id", ctx.myId, null, true); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple host property bindings in the presence of other bindings', () => { + const files = { + app: { + 'example.ts': ` + import {Directive} from '@angular/core'; + + @Directive({ + selector: '[my-dir]', + host: { + '[title]': '"my title"', + '[attr.tabindex]': '1', + '[id]': '"my-id"' + } + }) + export class MyDirective {}` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) { + … + if (rf & 2) { + $r3$.ɵɵproperty("title", "my title", null, true)("id", "my-id", null, true); + $r3$.ɵɵattribute("tabindex", 1); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple synthetic properties into a single instruction call', () => { + const files = { + app: { + 'example.ts': ` + import {Directive} from '@angular/core'; + + @Directive({ + selector: '[my-dir]', + host: { + '[@expand]': 'expandedState', + '[@fadeOut]': 'true', + '[@shrink]': 'isSmall' + } + }) + export class MyDirective { + expandedState = 'collapsed'; + isSmall = true; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) { + … + if (rf & 2) { + $r3$.ɵɵupdateSyntheticHostBinding("@expand", ctx.expandedState, null, true)("@fadeOut", true, null, true)("@shrink", ctx.isSmall, null, true); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple host attribute bindings into a single instruction', () => { + const files = { + app: { + 'example.ts': ` + import {Directive} from '@angular/core'; + + @Directive({ + selector: '[my-dir]', + host: { + '[attr.title]': 'myTitle', + '[attr.tabindex]': '1', + '[attr.id]': 'myId' + } + }) + export class MyDirective { + myTitle = 'hello'; + myId = 'special-directive'; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) { + … + if (rf & 2) { + $r3$.ɵɵattribute("title", ctx.myTitle)("tabindex", 1)("id", ctx.myId); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain both host attributes in the decorator and on the class', () => { + const files = { + app: { + 'example.ts': ` + import {Directive, HostBinding} from '@angular/core'; + + @Directive({ + selector: '[my-dir]', + host: { + '[attr.tabindex]': '1' + } + }) + export class MyDirective { + @HostBinding('attr.title') + myTitle = 'hello'; + + @HostBinding('attr.id') + myId = 'special-directive'; + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) { + … + if (rf & 2) { + $r3$.ɵɵattribute("tabindex", 1)("title", ctx.myTitle)("id", ctx.myId); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple host attribute bindings in the presence of other bindings', () => { + const files = { + app: { + 'example.ts': ` + import {Directive} from '@angular/core'; + + @Directive({ + selector: '[my-dir]', + host: { + '[attr.title]': '"my title"', + '[tabindex]': '1', + '[attr.id]': '"my-id"' + } + }) + export class MyDirective {}` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) { + … + if (rf & 2) { + $r3$.ɵɵproperty("tabindex", 1, null, true); + $r3$.ɵɵattribute("title", "my title")("id", "my-id"); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + }); describe('non bindable behavior', () => { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index ab4058c47a..9370eac221 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -1388,8 +1388,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer); } if (rf & 2) { - $r3$.ɵɵproperty("id", ctx.id, null, true); - $r3$.ɵɵproperty("title", ctx.title, null, true); + $r3$.ɵɵproperty("id", ctx.id, null, true)("title", ctx.title, null, true); $r3$.ɵɵstyleMap(ctx.myStyle); $r3$.ɵɵclassMap(ctx.myClass); $r3$.ɵɵstylingApply(); @@ -1435,8 +1434,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵstyling($_c0$, $_c1$); } if (rf & 2) { - $r3$.ɵɵproperty("id", ctx.id, null, true); - $r3$.ɵɵproperty("title", ctx.title, null, true); + $r3$.ɵɵproperty("id", ctx.id, null, true)("title", ctx.title, null, true); $r3$.ɵɵstyleProp(0, ctx.myWidth); $r3$.ɵɵclassProp(0, ctx.myFooClass); $r3$.ɵɵstylingApply(); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index d239b66842..e6bac33ec2 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -1795,8 +1795,8 @@ runInEachFileSystem(os => { i0.ɵɵstyling(_c0); } if (rf & 2) { - i0.ɵɵattribute("hello", ctx.foo); i0.ɵɵproperty("prop", ctx.bar, null, true); + i0.ɵɵattribute("hello", ctx.foo); i0.ɵɵclassProp(0, ctx.someClass); i0.ɵɵstylingApply(); } @@ -3236,12 +3236,7 @@ runInEachFileSystem(os => { i0.ɵɵallocHostVars(6); } if (rf & 2) { - i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl); - i0.ɵɵattribute("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl); - i0.ɵɵattribute("action", ctx.attrAction, i0.ɵɵsanitizeUrl); - i0.ɵɵattribute("profile", ctx.attrProfile, i0.ɵɵsanitizeResourceUrl); - i0.ɵɵattribute("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml); - i0.ɵɵattribute("title", ctx.attrSafeTitle); + i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.attrAction, i0.ɵɵsanitizeUrl)("profile", ctx.attrProfile, i0.ɵɵsanitizeResourceUrl)("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.attrSafeTitle); } } `; @@ -3291,12 +3286,7 @@ runInEachFileSystem(os => { i0.ɵɵallocHostVars(6); } if (rf & 2) { - i0.ɵɵproperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl, true); - i0.ɵɵproperty("src", ctx.propSrc, i0.ɵɵsanitizeUrlOrResourceUrl, true); - i0.ɵɵproperty("action", ctx.propAction, i0.ɵɵsanitizeUrl, true); - i0.ɵɵproperty("profile", ctx.propProfile, i0.ɵɵsanitizeResourceUrl, true); - i0.ɵɵproperty("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml, true); - i0.ɵɵproperty("title", ctx.propSafeTitle, null, true); + i0.ɵɵproperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl, true)("src", ctx.propSrc, i0.ɵɵsanitizeUrlOrResourceUrl, true)("action", ctx.propAction, i0.ɵɵsanitizeUrl, true)("profile", ctx.propProfile, i0.ɵɵsanitizeResourceUrl, true)("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml, true)("title", ctx.propSafeTitle, null, true); } } `; @@ -3331,12 +3321,8 @@ runInEachFileSystem(os => { i0.ɵɵallocHostVars(6); } if (rf & 2) { - i0.ɵɵproperty("src", ctx.srcProp, null, true); - i0.ɵɵproperty("href", ctx.hrefProp, null, true); - i0.ɵɵproperty("title", ctx.titleProp, null, true); - i0.ɵɵattribute("src", ctx.srcAttr); - i0.ɵɵattribute("href", ctx.hrefAttr); - i0.ɵɵattribute("title", ctx.titleAttr); + i0.ɵɵproperty("src", ctx.srcProp, null, true)("href", ctx.hrefProp, null, true)("title", ctx.titleProp, null, true); + i0.ɵɵattribute("src", ctx.srcAttr)("href", ctx.hrefAttr)("title", ctx.titleAttr); } } `; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 9ec1f5a2b4..3b364617f1 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -30,7 +30,7 @@ import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, type import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api'; import {Instruction, StylingBuilder} from './styling_builder'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template'; -import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; +import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, chainedInstruction, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; const EMPTY_ARRAY: any[] = []; @@ -638,6 +638,10 @@ function createHostBindingsFunction( // Calculate the host property bindings const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan); + const propertyBindings: o.Expression[][] = []; + const attributeBindings: o.Expression[][] = []; + const syntheticHostBindings: o.Expression[][] = []; + (bindings || []).forEach((binding: ParsedProperty) => { const name = binding.name; const stylingInputWasSet = @@ -681,10 +685,32 @@ function createHostBindingsFunction( } updateStatements.push(...bindingExpr.stmts); - updateStatements.push(o.importExpr(instruction).callFn(instructionParams).toStmt()); + + if (instruction === R3.property) { + propertyBindings.push(instructionParams); + } else if (instruction === R3.attribute) { + attributeBindings.push(instructionParams); + } else if (instruction === R3.updateSyntheticHostBinding) { + syntheticHostBindings.push(instructionParams); + } else { + updateStatements.push(o.importExpr(instruction).callFn(instructionParams).toStmt()); + } } }); + if (propertyBindings.length > 0) { + updateStatements.push(chainedInstruction(R3.property, propertyBindings).toStmt()); + } + + if (attributeBindings.length > 0) { + updateStatements.push(chainedInstruction(R3.attribute, attributeBindings).toStmt()); + } + + if (syntheticHostBindings.length > 0) { + updateStatements.push( + chainedInstruction(R3.updateSyntheticHostBinding, syntheticHostBindings).toStmt()); + } + // since we're dealing with directives/components and both have hostBinding // functions, we need to generate a special hostAttrs instruction that deals // with both the assignment of styling as well as static attributes to the host diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 926da4be14..f9c7bab668 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -37,7 +37,8 @@ import {I18nMetaVisitor} from './i18n/meta'; import {getSerializedI18nContent} from './i18n/serializer'; import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util'; import {Instruction, StylingBuilder} from './styling_builder'; -import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util'; +import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, chainedInstruction, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util'; + // Selector attribute name of `` @@ -1105,7 +1106,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver return fnParams; }); - return chainedInstruction(span, reference, calls).toStmt(); + return chainedInstruction(reference, calls, span).toStmt(); }); } @@ -1417,22 +1418,6 @@ function instruction( return o.importExpr(reference, null, span).callFn(params, span); } -function chainedInstruction( - span: ParseSourceSpan | null, reference: o.ExternalReference, calls: o.Expression[][]) { - let expression = o.importExpr(reference, null, span) as o.Expression; - - if (calls.length > 0) { - for (let i = 0; i < calls.length; i++) { - expression = expression.callFn(calls[i], span); - } - } else { - // Add a blank invocation, in case the `calls` array is empty. - expression = expression.callFn([], span); - } - - return expression; -} - // e.g. x(2); function generateNextContextExpr(relativeLevelDiff: number): o.Expression { return o.importExpr(R3.nextContext) diff --git a/packages/compiler/src/render3/view/util.ts b/packages/compiler/src/render3/view/util.ts index 9800e9d572..29881deefd 100644 --- a/packages/compiler/src/render3/view/util.ts +++ b/packages/compiler/src/render3/view/util.ts @@ -8,11 +8,14 @@ import {ConstantPool} from '../../constant_pool'; import * as o from '../../output/output_ast'; +import {ParseSourceSpan} from '../../parse_util'; import {splitAtColon} from '../../util'; import * as t from '../r3_ast'; + import {R3QueryMetadata} from './api'; import {isI18nAttribute} from './i18n/util'; + /** * Checks whether an object key contains potentially unsafe chars, thus the key should be wrapped in * quotes. Note: we do not wrap all keys into quotes, as it may have impact on minification and may @@ -181,3 +184,20 @@ export function getAttrsForDirectiveMatching(elOrTpl: t.Element | t.Template): return attributesMap; } + +/** Returns a call expression to a chained instruction, e.g. `property(params[0])(params[1])`. */ +export function chainedInstruction( + reference: o.ExternalReference, calls: o.Expression[][], span?: ParseSourceSpan | null) { + let expression = o.importExpr(reference, null, span) as o.Expression; + + if (calls.length > 0) { + for (let i = 0; i < calls.length; i++) { + expression = expression.callFn(calls[i], span); + } + } else { + // Add a blank invocation, in case the `calls` array is empty. + expression = expression.callFn([], span); + } + + return expression; +} diff --git a/packages/core/src/render3/instructions/property.ts b/packages/core/src/render3/instructions/property.ts index 77a91ba181..5a227de469 100644 --- a/packages/core/src/render3/instructions/property.ts +++ b/packages/core/src/render3/instructions/property.ts @@ -84,7 +84,8 @@ export function bind(lView: LView, value: T): T|NO_CHANGE { * @codeGenApi */ export function ɵɵupdateSyntheticHostBinding( - propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean) { + propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, + nativeOnly?: boolean): TsickleIssue1009 { const index = getSelectedIndex(); const lView = getLView(); // TODO(benlesh): remove bind call here. @@ -92,4 +93,5 @@ export function ɵɵupdateSyntheticHostBinding( if (bound !== NO_CHANGE) { elementPropertyInternal(index, propName, bound, sanitizer, nativeOnly, loadComponentRenderer); } + return ɵɵupdateSyntheticHostBinding; } diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 718b609ede..f42053504b 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -1075,7 +1075,7 @@ export declare function ɵɵtextInterpolate8(prefix: string, v0: any, i0: string export declare function ɵɵtextInterpolateV(values: any[]): TsickleIssue1009; -export declare function ɵɵupdateSyntheticHostBinding(propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): void; +export declare function ɵɵupdateSyntheticHostBinding(propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): TsickleIssue1009; export declare function ɵɵviewQuery(predicate: Type | string[], descend: boolean, read: any): QueryList;