Andrew Kushnir 6f203c9575 fix(ivy): handling className as an input properly (#33188)
Prior to this commit, all `className` inputs were not set because the runtime code assumed that the `classMap` instruction is only generated for `[class]` bindings. However the `[className]` binding also produces the same `classMap`, thus the code needs to distinguish between `class` and `className`. This commit adds extra logic to select the right input name and also throws an error in case `[class]` and `[className]` bindings are used on the same element simultaneously.

PR Close #33188
2019-10-17 14:16:02 -04:00

244 lines
9.7 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert';
import {assertHasParent} from '../assert';
import {attachPatchData} from '../context_discovery';
import {registerPostOrderHooks} from '../hooks';
import {TAttributes, TNodeFlags, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {StylingMapArray, TStylingContext} from '../interfaces/styling';
import {isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks';
import {BINDING_INDEX, HEADER_OFFSET, LView, RENDERER, TVIEW, T_HOST} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {appendChild} from '../node_manipulation';
import {decreaseElementDepthCount, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state';
import {setUpAttributes} from '../util/attrs_utils';
import {getInitialStylingValue, hasClassInput, hasStyleInput, selectClassBasedInputName} from '../util/styling_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils';
import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, renderInitialStyling, resolveDirectives, saveResolvedLocalsInData, setInputsForProperty} from './shared';
import {registerInitialStylingOnTNode} from './styling';
/**
* Create DOM element. The instruction must later be followed by `elementEnd()` call.
*
* @param index Index of the element in the LView array
* @param name Name of the DOM Node
* @param constsIndex Index of the element in the `consts` array.
* @param localRefs A set of local reference bindings on the element.
*
* Attributes and localRefs are passed as an array of strings where elements with an even index
* hold an attribute name and elements with an odd index hold an attribute value, ex.:
* ['id', 'warning5', 'class', 'alert']
*
* @codeGenApi
*/
export function ɵɵelementStart(
index: number, name: string, constsIndex?: number | null, localRefs?: string[] | null): void {
const lView = getLView();
const tView = lView[TVIEW];
const tViewConsts = tView.consts;
const consts = tViewConsts === null || constsIndex == null ? null : tViewConsts[constsIndex];
ngDevMode && assertEqual(
lView[BINDING_INDEX], tView.bindingStartIndex,
'elements should be created before any bindings');
ngDevMode && ngDevMode.rendererCreateElement++;
ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET);
const renderer = lView[RENDERER];
const native = lView[index + HEADER_OFFSET] = elementCreate(name, renderer, getNamespace());
const tNode = getOrCreateTNode(tView, lView[T_HOST], index, TNodeType.Element, name, consts);
if (consts != null) {
const lastAttrIndex = setUpAttributes(renderer, native, consts);
if (tView.firstTemplatePass) {
registerInitialStylingOnTNode(tNode, consts, lastAttrIndex);
}
}
if ((tNode.flags & TNodeFlags.hasInitialStyling) === TNodeFlags.hasInitialStyling) {
renderInitialStyling(renderer, native, tNode);
}
appendChild(native, tNode, lView);
// any immediate children of a component or template container must be pre-emptively
// monkey-patched with the component view data so that the element can be inspected
// later on using any element discovery utility methods (see `element_discovery.ts`)
if (getElementDepthCount() === 0) {
attachPatchData(native, lView);
}
increaseElementDepthCount();
// if a directive contains a host binding for "class" then all class-based data will
// flow through that (except for `[class.prop]` bindings). This also includes initial
// static class values as well. (Note that this will be fixed once map-based `[style]`
// and `[class]` bindings work for multiple directives.)
if (tView.firstTemplatePass) {
ngDevMode && ngDevMode.firstTemplatePass++;
resolveDirectives(tView, lView, tNode, localRefs || null);
if (tView.queries !== null) {
tView.queries.elementStart(tView, tNode);
}
}
if (isDirectiveHost(tNode)) {
createDirectivesInstances(tView, lView, tNode);
executeContentQueries(tView, tNode, lView);
}
if (localRefs != null) {
saveResolvedLocalsInData(lView, tNode);
}
}
/**
* Mark the end of the element.
*
* @codeGenApi
*/
export function ɵɵelementEnd(): void {
let previousOrParentTNode = getPreviousOrParentTNode();
ngDevMode && assertDefined(previousOrParentTNode, 'No parent node to close.');
if (getIsParent()) {
setIsNotParent();
} else {
ngDevMode && assertHasParent(getPreviousOrParentTNode());
previousOrParentTNode = previousOrParentTNode.parent !;
setPreviousOrParentTNode(previousOrParentTNode, false);
}
const tNode = previousOrParentTNode;
ngDevMode && assertNodeType(tNode, TNodeType.Element);
const lView = getLView();
const tView = lView[TVIEW];
decreaseElementDepthCount();
if (tView.firstTemplatePass) {
registerPostOrderHooks(tView, previousOrParentTNode);
if (isContentQueryHost(previousOrParentTNode)) {
tView.queries !.elementEnd(previousOrParentTNode);
}
}
if (hasClassInput(tNode)) {
const inputName: string = selectClassBasedInputName(tNode.inputs !);
setDirectiveStylingInput(tNode.classes, lView, tNode.inputs ![inputName]);
}
if (hasStyleInput(tNode)) {
setDirectiveStylingInput(tNode.styles, lView, tNode.inputs !['style']);
}
}
/**
* Creates an empty element using {@link elementStart} and {@link elementEnd}
*
* @param index Index of the element in the data array
* @param name Name of the DOM Node
* @param constsIndex Index of the element in the `consts` array.
* @param localRefs A set of local reference bindings on the element.
*
* @codeGenApi
*/
export function ɵɵelement(
index: number, name: string, constsIndex?: number | null, localRefs?: string[] | null): void {
ɵɵelementStart(index, name, constsIndex, localRefs);
ɵɵelementEnd();
}
/**
* Assign static attribute values to a host element.
*
* This instruction will assign static attribute values as well as class and style
* values to an element within the host bindings function. Since attribute values
* can consist of different types of values, the `attrs` array must include the values in
* the following format:
*
* attrs = [
* // static attributes (like `title`, `name`, `id`...)
* attr1, value1, attr2, value,
*
* // a single namespace value (like `x:id`)
* NAMESPACE_MARKER, namespaceUri1, name1, value1,
*
* // another single namespace value (like `x:name`)
* NAMESPACE_MARKER, namespaceUri2, name2, value2,
*
* // a series of CSS classes that will be applied to the element (no spaces)
* CLASSES_MARKER, class1, class2, class3,
*
* // a series of CSS styles (property + value) that will be applied to the element
* STYLES_MARKER, prop1, value1, prop2, value2
* ]
*
* All non-class and non-style attributes must be defined at the start of the list
* first before all class and style values are set. When there is a change in value
* type (like when classes and styles are introduced) a marker must be used to separate
* the entries. The marker values themselves are set via entries found in the
* [AttributeMarker] enum.
*
* NOTE: This instruction is meant to used from `hostBindings` function only.
*
* @param directive A directive instance the styling is associated with.
* @param attrs An array of static values (attributes, classes and styles) with the correct marker
* values.
*
* @codeGenApi
*/
export function ɵɵelementHostAttrs(attrs: TAttributes) {
const hostElementIndex = getSelectedIndex();
const lView = getLView();
const tView = lView[TVIEW];
const tNode = getTNode(hostElementIndex, lView);
// non-element nodes (e.g. `<ng-container>`) are not rendered as actual
// element nodes and adding styles/classes on to them will cause runtime
// errors...
if (tNode.type === TNodeType.Element) {
const native = getNativeByTNode(tNode, lView) as RElement;
const lastAttrIndex = setUpAttributes(lView[RENDERER], native, attrs);
if (tView.firstTemplatePass) {
const stylingNeedsToBeRendered = registerInitialStylingOnTNode(tNode, attrs, lastAttrIndex);
// this is only called during the first template pass in the
// event that this current directive assigned initial style/class
// host attribute values to the element. Because initial styling
// values are applied before directives are first rendered (within
// `createElement`) this means that initial styling for any directives
// still needs to be applied. Note that this will only happen during
// the first template pass and not each time a directive applies its
// attribute values to the element.
if (stylingNeedsToBeRendered) {
const renderer = lView[RENDERER];
renderInitialStyling(renderer, native, tNode);
}
}
}
}
function setDirectiveStylingInput(
context: TStylingContext | StylingMapArray | null, lView: LView,
stylingInputs: (string | number)[]) {
// older versions of Angular treat the input as `null` in the
// event that the value does not exist at all. For this reason
// we can't have a styling value be an empty string.
const value = (context && getInitialStylingValue(context)) || null;
// Ivy does an extra `[class]` write with a falsy value since the value
// is applied during creation mode. This is a deviation from VE and should
// be (Jira Issue = FW-1467).
setInputsForProperty(lView, stylingInputs, value);
}