refactor(ivy): remove def.attributes in favor of the elementHostAttrs instruction (#28089)

Up until this point, all static attribute values (things like `title` and `id`)
defined within the `host` are of a Component/Directive definition were
generated into a `def.attributes` array and then processed at runtime.
This design decision does not lend itself well to tree-shaking and is
inconsistent with other static values such as styles and classes.

This fix ensures that all static attribute values (attributes, classes,
and styles) that exist within a host definition for components and
directives are all assigned via the `elementHostAttrs` instruction.

```
// before
defineDirective({
  ...
  attributes: ['title', 'my title']
  ...
})

//now
defineDirective({
  ...
  hostBindings: function() {
    if (create) {
      elementHostAttrs(..., ['title', 'my-title']);
    }
    ...
  }
  ...
})
```

PR Close #28089
This commit is contained in:
Matias Niemelä
2019-01-11 14:03:37 -08:00
committed by Andrew Kushnir
parent e62eeed7d4
commit 693045165c
14 changed files with 226 additions and 127 deletions

View File

@ -29,7 +29,7 @@ import {Render3ParseResult} from '../r3_template_transform';
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
import {StylingBuilder, StylingInstruction} from './styling_builder';
import {Instruction, StylingBuilder} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
@ -101,14 +101,11 @@ function baseDirectiveFields(
}
}
// e.g. `attributes: ['role', 'listbox']`
definitionMap.set('attributes', createHostAttributesArray(allOtherAttributes));
// e.g. `hostBindings: (rf, ctx, elIndex) => { ... }
definitionMap.set(
'hostBindings',
createHostBindingsFunction(
meta, elVarExp, contextVarExp, styleBuilder, bindingParser, constantPool, hostVarsCount));
'hostBindings', createHostBindingsFunction(
meta, elVarExp, contextVarExp, allOtherAttributes, styleBuilder,
bindingParser, constantPool, hostVarsCount));
// e.g 'inputs: {a: 'a'}`
definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs, true));
@ -504,16 +501,13 @@ function createDirectiveSelector(selector: string | null): o.Expression {
return asLiteral(core.parseSelectorToR3Selector(selector));
}
function createHostAttributesArray(attributes: any): o.Expression|null {
function convertAttributesToExpressions(attributes: any): o.Expression[] {
const values: o.Expression[] = [];
for (let key of Object.getOwnPropertyNames(attributes)) {
const value = attributes[key];
values.push(o.literal(key), o.literal(value));
}
if (values.length > 0) {
return o.literalArr(values);
}
return null;
return values;
}
// Return a contentQueries function or null if one is not necessary.
@ -649,8 +643,8 @@ function createViewQueriesFunction(
// Return a host binding function or null if one is not necessary.
function createHostBindingsFunction(
meta: R3DirectiveMetadata, elVarExp: o.ReadVarExpr, bindingContext: o.ReadVarExpr,
styleBuilder: StylingBuilder, bindingParser: BindingParser, constantPool: ConstantPool,
hostVarsCount: number): o.Expression|null {
staticAttributesAndValues: any[], styleBuilder: StylingBuilder, bindingParser: BindingParser,
constantPool: ConstantPool, hostVarsCount: number): o.Expression|null {
const createStatements: o.Statement[] = [];
const updateStatements: o.Statement[] = [];
@ -740,18 +734,20 @@ function createHostBindingsFunction(
}
}
if (styleBuilder.hasBindingsOrInitialValues()) {
// since we're dealing with directives here and directives have a hostBinding
// function, we need to generate special instructions that deal with styling
// (both bindings and initial values). The instruction below will instruct
// all initial styling (styling that is inside of a host binding within a
// directive) to be attached to the host element of the directive.
const hostAttrsInstruction =
styleBuilder.buildDirectiveHostAttrsInstruction(null, constantPool);
if (hostAttrsInstruction) {
createStatements.push(createStylingStmt(hostAttrsInstruction, bindingContext, bindingFn));
}
// 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
// element. The instruction below will instruct all initial styling (styling
// that is inside of a host binding within a directive/component) to be attached
// to the host element alongside any of the provided host attributes that were
// collected earlier.
const hostAttrs = convertAttributesToExpressions(staticAttributesAndValues);
const hostInstruction = styleBuilder.buildHostAttrsInstruction(null, hostAttrs, constantPool);
if (hostInstruction) {
createStatements.push(createStylingStmt(hostInstruction, bindingContext, bindingFn));
}
if (styleBuilder.hasBindingsOrInitialValues()) {
// singular style/class bindings (things like `[style.prop]` and `[class.name]`)
// MUST be registered on a given element within the component/directive
// templateFn/hostBindingsFn functions. The instruction below will figure out
@ -799,7 +795,7 @@ function createHostBindingsFunction(
}
function createStylingStmt(
instruction: StylingInstruction, bindingContext: any, bindingFn: Function): o.Statement {
instruction: Instruction, bindingContext: any, bindingFn: Function): o.Statement {
const params = instruction.buildParams(value => bindingFn(bindingContext, value).currValExpr);
return o.importExpr(instruction.reference, null, instruction.sourceSpan)
.callFn(params, instruction.sourceSpan)

View File

@ -20,7 +20,7 @@ import {ValueConverter} from './template';
/**
* A styling expression summary that is to be processed by the compiler
*/
export interface StylingInstruction {
export interface Instruction {
sourceSpan: ParseSourceSpan|null;
reference: o.ExternalReference;
buildParams(convertFn: (value: any) => o.Expression): o.Expression[];
@ -225,17 +225,17 @@ export class StylingBuilder {
* Builds an instruction with all the expressions and parameters for `elementHostAttrs`.
*
* The instruction generation code below is used for producing the AOT statement code which is
* responsible for registering initial styles (within a directive hostBindings' creation block)
* to the directive host element.
* responsible for registering initial styles (within a directive hostBindings' creation block),
* as well as any of the provided attribute values, to the directive host element.
*/
buildDirectiveHostAttrsInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
StylingInstruction|null {
if (this._hasInitialValues && this._directiveExpr) {
buildHostAttrsInstruction(
sourceSpan: ParseSourceSpan|null, attrs: o.Expression[],
constantPool: ConstantPool): Instruction|null {
if (this._directiveExpr && (attrs.length || this._hasInitialValues)) {
return {
sourceSpan,
reference: R3.elementHostAttrs,
buildParams: () => {
const attrs: o.Expression[] = [];
this.populateInitialStylingAttrs(attrs);
return [this._directiveExpr !, getConstantLiteralFromArray(constantPool, attrs)];
}
@ -251,7 +251,7 @@ export class StylingBuilder {
* responsible for registering style/class bindings to an element.
*/
buildElementStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
StylingInstruction|null {
Instruction|null {
if (this._hasBindings) {
return {
sourceSpan,
@ -312,7 +312,7 @@ export class StylingBuilder {
* which include the `[style]` and `[class]` expression params (if they exist) as well as
* the sanitizer and directive reference expression.
*/
buildElementStylingMapInstruction(valueConverter: ValueConverter): StylingInstruction|null {
buildElementStylingMapInstruction(valueConverter: ValueConverter): Instruction|null {
if (this._classMapInput || this._styleMapInput) {
const stylingInput = this._classMapInput ! || this._styleMapInput !;
@ -355,7 +355,7 @@ export class StylingBuilder {
private _buildSingleInputs(
reference: o.ExternalReference, inputs: BoundStylingEntry[], mapIndex: Map<string, number>,
allowUnits: boolean, valueConverter: ValueConverter): StylingInstruction[] {
allowUnits: boolean, valueConverter: ValueConverter): Instruction[] {
return inputs.map(input => {
const bindingIndex: number = mapIndex.get(input.name) !;
const value = input.value.visit(valueConverter);
@ -381,7 +381,7 @@ export class StylingBuilder {
});
}
private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] {
private _buildClassInputs(valueConverter: ValueConverter): Instruction[] {
if (this._singleClassInputs) {
return this._buildSingleInputs(
R3.elementClassProp, this._singleClassInputs, this._classesIndex, false, valueConverter);
@ -389,7 +389,7 @@ export class StylingBuilder {
return [];
}
private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] {
private _buildStyleInputs(valueConverter: ValueConverter): Instruction[] {
if (this._singleStyleInputs) {
return this._buildSingleInputs(
R3.elementStyleProp, this._singleStyleInputs, this._stylesIndex, true, valueConverter);
@ -397,7 +397,7 @@ export class StylingBuilder {
return [];
}
private _buildApplyFn(): StylingInstruction {
private _buildApplyFn(): Instruction {
return {
sourceSpan: this._lastStylingInput ? this._lastStylingInput.sourceSpan : null,
reference: R3.elementStylingApply,
@ -416,7 +416,7 @@ export class StylingBuilder {
* into the update block of a template function or a directive hostBindings function.
*/
buildUpdateLevelInstructions(valueConverter: ValueConverter) {
const instructions: StylingInstruction[] = [];
const instructions: Instruction[] = [];
if (this._hasBindings) {
const mapInstruction = this.buildElementStylingMapInstruction(valueConverter);
if (mapInstruction) {

View File

@ -36,7 +36,7 @@ import {I18nContext} from './i18n/context';
import {I18nMetaVisitor} from './i18n/meta';
import {getSerializedI18nContent} from './i18n/serializer';
import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
import {StylingBuilder, StylingInstruction} from './styling_builder';
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';
// Default selector used by `<ng-content>` if none specified
@ -689,7 +689,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (input.type === BindingType.Animation) {
const value = input.value.visit(this._valueConverter);
// animation bindings can be presented in the following formats:
// 1j [@binding]="fooExp"
// 1. [@binding]="fooExp"
// 2. [@binding]="{value:fooExp, params:{...}}"
// 3. [@binding]
// 4. @binding
@ -936,7 +936,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}
private processStylingInstruction(
implicit: any, instruction: StylingInstruction|null, createMode: boolean) {
implicit: any, instruction: Instruction|null, createMode: boolean) {
if (instruction) {
const paramsFn = () =>
instruction.buildParams(value => this.convertPropertyBinding(implicit, value, true));