Skip to content

Commit 297dc2b

Browse files
committed
fix(ivy): ensure ngClass works with [class] bindings (angular#26559)
PR Close angular#26559
1 parent 0cc9842 commit 297dc2b

File tree

12 files changed

+199
-49
lines changed

12 files changed

+199
-49
lines changed

packages/core/src/render3/i18n.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {NO_CHANGE} from '../../src/render3/tokens';
10+
911
import {assertEqual, assertLessThan} from './assert';
10-
import {NO_CHANGE, _getViewData, adjustBlueprintForNewNode, bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, createNodeAtIndex, getRenderer, load, resetComponentState} from './instructions';
12+
import {_getViewData, adjustBlueprintForNewNode, bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, createNodeAtIndex, getRenderer, load, resetComponentState} from './instructions';
1113
import {LContainer, NATIVE, RENDER_PARENT} from './interfaces/container';
1214
import {TElementNode, TNode, TNodeType} from './interfaces/node';
1315
import {RComment, RElement} from './interfaces/renderer';

packages/core/src/render3/index.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ export {CssSelectorList} from './interfaces/projection';
2020

2121
// clang-format off
2222
export {
23-
24-
NO_CHANGE,
25-
2623
bind,
2724
interpolation1,
2825
interpolation2,
@@ -175,3 +172,5 @@ export {
175172
renderComponent,
176173
whenRendered,
177174
};
175+
176+
export {NO_CHANGE} from './tokens';

packages/core/src/render3/instructions.ts

+48-22
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ import {PlayerFactory} from './interfaces/player';
2424
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
2525
import {LQueries} from './interfaces/query';
2626
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
27+
import {StylingIndex} from './interfaces/styling';
2728
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, CurrentMatchesList, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view';
2829
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
2930
import {appendChild, appendProjectedNode, createTextNode, findComponentView, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation';
3031
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
3132
import {createStylingContextTemplate, renderStyleAndClassBindings, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
3233
import {BoundPlayerFactory} from './styling/player_factory';
3334
import {getStylingContext} from './styling/util';
35+
import {NO_CHANGE} from './tokens';
3436
import {assertDataInRangeInternal, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isContentQueryHost, isDifferent, loadInternal, readPatchedLViewData, stringify} from './util';
3537

3638

@@ -1337,14 +1339,7 @@ export function elementProperty<T>(
13371339
if (value === NO_CHANGE) return;
13381340
const element = getNativeByIndex(index, viewData) as RElement | RComment;
13391341
const tNode = getTNode(index, viewData);
1340-
// if tNode.inputs is undefined, a listener has created outputs, but inputs haven't
1341-
// yet been checked
1342-
if (tNode && tNode.inputs === undefined) {
1343-
// mark inputs as checked
1344-
tNode.inputs = generatePropertyAliases(tNode.flags, BindingDirection.Input);
1345-
}
1346-
1347-
const inputData = tNode && tNode.inputs;
1342+
const inputData = initializeTNodeInputs(tNode);
13481343
let dataValue: PropertyAliasValue|undefined;
13491344
if (inputData && (dataValue = inputData[propName])) {
13501345
setInputsForProperty(dataValue, value);
@@ -1543,14 +1538,28 @@ export function elementStyling(
15431538
styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
15441539
styleSanitizer?: StyleSanitizeFn | null): void {
15451540
const tNode = previousOrParentTNode;
1541+
const inputData = initializeTNodeInputs(tNode);
1542+
15461543
if (!tNode.stylingTemplate) {
1544+
const hasClassInput = inputData && inputData.hasOwnProperty('class') ? true : false;
1545+
if (hasClassInput) {
1546+
tNode.flags |= TNodeFlags.hasClassInput;
1547+
}
1548+
15471549
// initialize the styling template.
1548-
tNode.stylingTemplate =
1549-
createStylingContextTemplate(classDeclarations, styleDeclarations, styleSanitizer);
1550+
tNode.stylingTemplate = createStylingContextTemplate(
1551+
classDeclarations, styleDeclarations, styleSanitizer, hasClassInput);
15501552
}
1553+
15511554
if (styleDeclarations && styleDeclarations.length ||
15521555
classDeclarations && classDeclarations.length) {
1553-
elementStylingApply(tNode.index - HEADER_OFFSET);
1556+
const index = tNode.index - HEADER_OFFSET;
1557+
if (delegateToClassInput(tNode)) {
1558+
const stylingContext = getStylingContext(index, viewData);
1559+
const initialClasses = stylingContext[StylingIndex.PreviousOrCachedMultiClassValue] as string;
1560+
setInputsForProperty(tNode.inputs !['class'] !, initialClasses);
1561+
}
1562+
elementStylingApply(index);
15541563
}
15551564
}
15561565

@@ -1640,9 +1649,17 @@ export function elementStyleProp(
16401649
* removed (unset) from the element's styling.
16411650
*/
16421651
export function elementStylingMap<T>(
1643-
index: number, classes: {[key: string]: any} | string | null,
1644-
styles?: {[styleName: string]: any} | null): void {
1645-
updateStylingMap(getStylingContext(index, viewData), classes, styles);
1652+
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
1653+
styles?: {[styleName: string]: any} | NO_CHANGE | null): void {
1654+
const tNode = getTNode(index, viewData);
1655+
const stylingContext = getStylingContext(index, viewData);
1656+
if (delegateToClassInput(tNode) && classes !== NO_CHANGE) {
1657+
const initialClasses = stylingContext[StylingIndex.PreviousOrCachedMultiClassValue] as string;
1658+
const classInputVal =
1659+
(initialClasses.length ? (initialClasses + ' ') : '') + (classes as string);
1660+
setInputsForProperty(tNode.inputs !['class'] !, classInputVal);
1661+
}
1662+
updateStylingMap(stylingContext, classes, styles);
16461663
}
16471664

16481665
//////////////////////////
@@ -2577,14 +2594,6 @@ export function markDirty<T>(component: T) {
25772594
//// Bindings & interpolations
25782595
///////////////////////////////
25792596

2580-
export interface NO_CHANGE {
2581-
// This is a brand that ensures that this type can never match anything else
2582-
brand: 'NO_CHANGE';
2583-
}
2584-
2585-
/** A special value which designates that a value has not changed. */
2586-
export const NO_CHANGE = {} as NO_CHANGE;
2587-
25882597
/**
25892598
* Creates a single value binding.
25902599
*
@@ -2871,3 +2880,20 @@ function assertDataNext(index: number, arr?: any[]) {
28712880
}
28722881

28732882
export const CLEAN_PROMISE = _CLEAN_PROMISE;
2883+
2884+
function initializeTNodeInputs(tNode: TNode | null) {
2885+
// If tNode.inputs is undefined, a listener has created outputs, but inputs haven't
2886+
// yet been checked.
2887+
if (tNode) {
2888+
if (tNode.inputs === undefined) {
2889+
// mark inputs as checked
2890+
tNode.inputs = generatePropertyAliases(tNode.flags, BindingDirection.Input);
2891+
}
2892+
return tNode.inputs;
2893+
}
2894+
return null;
2895+
}
2896+
2897+
export function delegateToClassInput(tNode: TNode) {
2898+
return tNode.flags & TNodeFlags.hasClassInput;
2899+
}

packages/core/src/render3/interfaces/node.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ export const enum TNodeFlags {
3939
/** This bit is set if the node has any content queries */
4040
hasContentQuery = 0b00000000000000000100000000000000,
4141

42+
/** This bit is set if the node has any directives that contain [class properties */
43+
hasClassInput = 0b00000000000000001000000000000000,
44+
4245
/** The index of the first directive on this node is encoded on the most significant bits */
43-
DirectiveStartingIndexShift = 15,
46+
DirectiveStartingIndexShift = 16,
4447
}
4548

4649
/**

packages/core/src/render3/interfaces/styling.ts

+14-10
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export interface StylingContext extends Array<InitialStyles|{[key: string]: any}
156156
* The last class value that was interpreted by elementStylingMap. This is cached
157157
* So that the algorithm can exit early incase the value has not changed.
158158
*/
159-
[StylingIndex.PreviousMultiClassValue]: {[key: string]: any}|string|null;
159+
[StylingIndex.PreviousOrCachedMultiClassValue]: {[key: string]: any}|string|null;
160160

161161
/**
162162
* The last style value that was interpreted by elementStylingMap. This is cached
@@ -181,18 +181,21 @@ export interface InitialStyles extends Array<string|null|boolean> { [0]: null; }
181181
*/
182182
export const enum StylingFlags {
183183
// Implies no configurations
184-
None = 0b0000,
184+
None = 0b00000,
185185
// Whether or not the entry or context itself is dirty
186-
Dirty = 0b0001,
186+
Dirty = 0b00001,
187187
// Whether or not this is a class-based assignment
188-
Class = 0b0010,
188+
Class = 0b00010,
189189
// Whether or not a sanitizer was applied to this property
190-
Sanitize = 0b0100,
190+
Sanitize = 0b00100,
191191
// Whether or not any player builders within need to produce new players
192-
PlayerBuildersDirty = 0b1000,
192+
PlayerBuildersDirty = 0b01000,
193+
// If NgClass is present (or some other class handler) then it will handle the map expressions and
194+
// initial classes
195+
OnlyProcessSingleClasses = 0b10000,
193196
// The max amount of bits used to represent these configuration values
194-
BitCountSize = 4,
195-
// There are only three bits here
197+
BitCountSize = 5,
198+
// There are only five bits here
196199
BitMask = 0b1111
197200
}
198201

@@ -211,8 +214,9 @@ export const enum StylingIndex {
211214
// Position of where the initial styles are stored in the styling context
212215
// This index must align with HOST, see interfaces/view.ts
213216
ElementPosition = 5,
214-
// Position of where the last string-based CSS class value was stored
215-
PreviousMultiClassValue = 6,
217+
// Position of where the last string-based CSS class value was stored (or a cached version of the
218+
// initial styles when a [class] directive is present)
219+
PreviousOrCachedMultiClassValue = 6,
216220
// Position of where the last string-based CSS class value was stored
217221
PreviousMultiStyleValue = 7,
218222
// Location of single (prop) value entries are stored within the context

packages/core/src/render3/styling/class_and_style_bindings.ts

+26-9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerI
1111
import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
1212
import {InitialStyles, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
1313
import {LViewData, RootContext} from '../interfaces/view';
14+
import {NO_CHANGE} from '../tokens';
1415
import {getRootContext} from '../util';
1516

1617
import {BoundPlayerFactory} from './player_factory';
@@ -45,7 +46,7 @@ const EMPTY_OBJ: {[key: string]: any} = {};
4546
export function createStylingContextTemplate(
4647
initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
4748
initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
48-
styleSanitizer?: StyleSanitizeFn | null): StylingContext {
49+
styleSanitizer?: StyleSanitizeFn | null, onlyProcessSingleClasses?: boolean): StylingContext {
4950
const initialStylingValues: InitialStyles = [null];
5051
const context: StylingContext =
5152
createEmptyStylingContext(null, styleSanitizer, initialStylingValues);
@@ -80,6 +81,7 @@ export function createStylingContextTemplate(
8081
// make where the class offsets begin
8182
context[StylingIndex.ClassOffsetPosition] = totalStyleDeclarations;
8283

84+
const initialStaticClasses: string[]|null = onlyProcessSingleClasses ? [] : null;
8385
if (initialClassDeclarations) {
8486
let hasPassedDeclarations = false;
8587
for (let i = 0; i < initialClassDeclarations.length; i++) {
@@ -93,6 +95,7 @@ export function createStylingContextTemplate(
9395
const value = initialClassDeclarations[++i] as boolean;
9496
initialStylingValues.push(value);
9597
classesLookup[className] = initialStylingValues.length - 1;
98+
initialStaticClasses && initialStaticClasses.push(className);
9699
} else {
97100
classesLookup[className] = 0;
98101
}
@@ -143,9 +146,15 @@ export function createStylingContextTemplate(
143146

144147
// there is no initial value flag for the master index since it doesn't
145148
// reference an initial style value
146-
setFlag(context, StylingIndex.MasterFlagPosition, pointers(0, 0, multiStart));
149+
const masterFlag = pointers(0, 0, multiStart) |
150+
(onlyProcessSingleClasses ? StylingFlags.OnlyProcessSingleClasses : 0);
151+
setFlag(context, StylingIndex.MasterFlagPosition, masterFlag);
147152
setContextDirty(context, initialStylingValues.length > 1);
148153

154+
if (initialStaticClasses) {
155+
context[StylingIndex.PreviousOrCachedMultiClassValue] = initialStaticClasses.join(' ');
156+
}
157+
149158
return context;
150159
}
151160

@@ -164,8 +173,8 @@ export function createStylingContextTemplate(
164173
*/
165174
export function updateStylingMap(
166175
context: StylingContext, classesInput: {[key: string]: any} | string |
167-
BoundPlayerFactory<null|string|{[key: string]: any}>| null,
168-
stylesInput?: {[key: string]: any} | BoundPlayerFactory<null|{[key: string]: any}>|
176+
BoundPlayerFactory<null|string|{[key: string]: any}>| NO_CHANGE | null,
177+
stylesInput?: {[key: string]: any} | BoundPlayerFactory<null|{[key: string]: any}>| NO_CHANGE |
169178
null): void {
170179
stylesInput = stylesInput || null;
171180

@@ -181,13 +190,14 @@ export function updateStylingMap(
181190
(classesInput as BoundPlayerFactory<{[key: string]: any}|string>) !.value :
182191
classesInput;
183192
const stylesValue = stylesPlayerBuilder ? stylesInput !.value : stylesInput;
184-
185193
// early exit (this is what's done to avoid using ctx.bind() to cache the value)
186-
const ignoreAllClassUpdates = classesValue === context[StylingIndex.PreviousMultiClassValue];
187-
const ignoreAllStyleUpdates = stylesValue === context[StylingIndex.PreviousMultiStyleValue];
194+
const ignoreAllClassUpdates = limitToSingleClasses(context) || classesValue === NO_CHANGE ||
195+
classesValue === context[StylingIndex.PreviousOrCachedMultiClassValue];
196+
const ignoreAllStyleUpdates =
197+
stylesValue === NO_CHANGE || stylesValue === context[StylingIndex.PreviousMultiStyleValue];
188198
if (ignoreAllClassUpdates && ignoreAllStyleUpdates) return;
189199

190-
context[StylingIndex.PreviousMultiClassValue] = classesValue;
200+
context[StylingIndex.PreviousOrCachedMultiClassValue] = classesValue;
191201
context[StylingIndex.PreviousMultiStyleValue] = stylesValue;
192202

193203
let classNames: string[] = EMPTY_ARR;
@@ -478,6 +488,8 @@ export function renderStyleAndClassBindings(
478488
const native = context[StylingIndex.ElementPosition] !;
479489
const multiStartIndex = getMultiStartIndex(context);
480490
const styleSanitizer = getStyleSanitizer(context);
491+
const onlySingleClasses = limitToSingleClasses(context);
492+
481493
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
482494
i += StylingIndex.Size) {
483495
// there is no point in rendering styles that have not changed on screen
@@ -488,6 +500,7 @@ export function renderStyleAndClassBindings(
488500
const playerBuilder = getPlayerBuilder(context, i);
489501
const isClassBased = flag & StylingFlags.Class ? true : false;
490502
const isInSingleRegion = i < multiStartIndex;
503+
const readInitialValue = !isClassBased || !onlySingleClasses;
491504

492505
let valueToApply: string|boolean|null = value;
493506

@@ -506,7 +519,7 @@ export function renderStyleAndClassBindings(
506519
// note that this should always be a falsy check since `false` is used
507520
// for both class and style comparisons (styles can't be false and false
508521
// classes are turned off and should therefore defer to their initial values)
509-
if (!valueExists(valueToApply, isClassBased)) {
522+
if (!valueExists(valueToApply, isClassBased) && readInitialValue) {
510523
valueToApply = getInitialValue(context, flag);
511524
}
512525

@@ -765,6 +778,10 @@ export function isContextDirty(context: StylingContext): boolean {
765778
return isDirty(context, StylingIndex.MasterFlagPosition);
766779
}
767780

781+
export function limitToSingleClasses(context: StylingContext) {
782+
return context[StylingIndex.MasterFlagPosition] & StylingFlags.OnlyProcessSingleClasses;
783+
}
784+
768785
export function setContextDirty(context: StylingContext, isDirtyYes: boolean): void {
769786
setDirty(context, StylingIndex.MasterFlagPosition, isDirtyYes);
770787
}

packages/core/src/render3/tokens.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export interface NO_CHANGE {
10+
// This is a brand that ensures that this type can never match anything else
11+
brand: 'NO_CHANGE';
12+
}
13+
14+
/** A special value which designates that a value has not changed. */
15+
export const NO_CHANGE = {} as NO_CHANGE;

packages/core/test/bundling/animation_world/bundle.golden_symbols.json

+9
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,9 @@
446446
{
447447
"name": "defineInjectable"
448448
},
449+
{
450+
"name": "delegateToClassInput"
451+
},
449452
{
450453
"name": "destroyLView"
451454
},
@@ -740,6 +743,9 @@
740743
{
741744
"name": "hasValueChanged"
742745
},
746+
{
747+
"name": "initializeTNodeInputs"
748+
},
743749
{
744750
"name": "inject"
745751
},
@@ -833,6 +839,9 @@
833839
{
834840
"name": "leaveView"
835841
},
842+
{
843+
"name": "limitToSingleClasses"
844+
},
836845
{
837846
"name": "listener"
838847
},

0 commit comments

Comments
 (0)