fix(ivy): ensure template styles/classes are applied before directives are instantiated (#29269)
Angular Ivy interprets inline static style/class attribute values as instructions that are processed whilst an element gets created. Because these inline style values are referenced by style/class bindings, their inline style values are applied at a later stage. Despite them being eventually applied, their values should be applied earlier before any directives are instantiated so that directive code can rely on any inline style/class changes. This patch ensures that all static style/class attribute values are applied (rendered) on the element before directives are instantiated. Jira Issue: FW-1133 PR Close #29269
This commit is contained in:
parent
1a9ab2727e
commit
1877e6c3f8
|
@ -619,10 +619,13 @@ export function elementStart(
|
|||
ngDevMode && ngDevMode.rendererCreateElement++;
|
||||
|
||||
const native = elementCreate(name);
|
||||
const renderer = lView[RENDERER];
|
||||
|
||||
ngDevMode && assertDataInRange(lView, index - 1);
|
||||
|
||||
const tNode = createNodeAtIndex(index, TNodeType.Element, native !, name, attrs || null);
|
||||
let initialStylesIndex = 0;
|
||||
let initialClassesIndex = 0;
|
||||
|
||||
if (attrs) {
|
||||
const lastAttrIndex = setUpAttributes(native, attrs);
|
||||
|
@ -640,6 +643,14 @@ export function elementStart(
|
|||
tNode.stylingTemplate = initializeStaticStylingContext(attrs, stylingAttrsStartIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (tNode.stylingTemplate) {
|
||||
// the initial style/class values are rendered immediately after having been
|
||||
// initialized into the context so the element styling is ready when directives
|
||||
// are initialized (since they may read style/class values in their constructor)
|
||||
initialStylesIndex = renderInitialStyles(native, tNode.stylingTemplate, renderer);
|
||||
initialClassesIndex = renderInitialClasses(native, tNode.stylingTemplate, renderer);
|
||||
}
|
||||
}
|
||||
|
||||
appendChild(native, tNode, lView);
|
||||
|
@ -667,11 +678,11 @@ export function elementStart(
|
|||
}
|
||||
}
|
||||
|
||||
// There is no point in rendering styles when a class directive is present since
|
||||
// it will take that over for us (this will be removed once #FW-882 is in).
|
||||
// we render the styling again below in case any directives have set any `style` and/or
|
||||
// `class` host attribute values...
|
||||
if (tNode.stylingTemplate) {
|
||||
renderInitialClasses(native, tNode.stylingTemplate, lView[RENDERER]);
|
||||
renderInitialStyles(native, tNode.stylingTemplate, lView[RENDERER]);
|
||||
renderInitialClasses(native, tNode.stylingTemplate, renderer, initialClassesIndex);
|
||||
renderInitialStyles(native, tNode.stylingTemplate, renderer, initialStylesIndex);
|
||||
}
|
||||
|
||||
const currentQueries = lView[QUERIES];
|
||||
|
|
|
@ -470,6 +470,12 @@ export const enum InitialStylingValuesIndex {
|
|||
*/
|
||||
DirectiveOwnerOffset = 2,
|
||||
|
||||
/**
|
||||
* The first bit set aside to mark if the initial style was already rendere
|
||||
*/
|
||||
AppliedFlagBitPosition = 0b0,
|
||||
AppliedFlagBitLength = 1,
|
||||
|
||||
/**
|
||||
* The total size for each style/class entry (prop + value + directiveOwner)
|
||||
*/
|
||||
|
|
|
@ -139,47 +139,53 @@ function patchInitialStylingValue(
|
|||
}
|
||||
|
||||
/**
|
||||
* Runs through the initial style data present in the context and renders
|
||||
* them via the renderer on the element.
|
||||
*/
|
||||
export function renderInitialStyles(
|
||||
element: RElement, context: StylingContext, renderer: Renderer3) {
|
||||
const initialStyles = context[StylingIndex.InitialStyleValuesPosition];
|
||||
renderInitialStylingValues(element, renderer, initialStyles, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs through the initial class data present in the context and renders
|
||||
* them via the renderer on the element.
|
||||
* Runs through the initial class values present in the provided
|
||||
* context and renders them via the provided renderer on the element.
|
||||
*
|
||||
* @param element the element the styling will be applied to
|
||||
* @param context the source styling context which contains the initial class values
|
||||
* @param renderer the renderer instance that will be used to apply the class
|
||||
* @returns the index that the classes were applied up until
|
||||
*/
|
||||
export function renderInitialClasses(
|
||||
element: RElement, context: StylingContext, renderer: Renderer3) {
|
||||
element: RElement, context: StylingContext, renderer: Renderer3, startIndex?: number): number {
|
||||
const initialClasses = context[StylingIndex.InitialClassValuesPosition];
|
||||
renderInitialStylingValues(element, renderer, initialClasses, true);
|
||||
let i = startIndex || InitialStylingValuesIndex.KeyValueStartPosition;
|
||||
while (i < initialClasses.length) {
|
||||
const value = initialClasses[i + InitialStylingValuesIndex.ValueOffset];
|
||||
if (value) {
|
||||
setClass(
|
||||
element, initialClasses[i + InitialStylingValuesIndex.PropOffset] as string, true,
|
||||
renderer, null);
|
||||
}
|
||||
i += InitialStylingValuesIndex.Size;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper function designed to render each entry present within the
|
||||
* provided list of initialStylingValues.
|
||||
* Runs through the initial styles values present in the provided
|
||||
* context and renders them via the provided renderer on the element.
|
||||
*
|
||||
* @param element the element the styling will be applied to
|
||||
* @param context the source styling context which contains the initial class values
|
||||
* @param renderer the renderer instance that will be used to apply the class
|
||||
* @returns the index that the styles were applied up until
|
||||
*/
|
||||
function renderInitialStylingValues(
|
||||
element: RElement, renderer: Renderer3, initialStylingValues: InitialStylingValues,
|
||||
isEntryClassBased: boolean) {
|
||||
for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialStylingValues.length;
|
||||
i += InitialStylingValuesIndex.Size) {
|
||||
const value = initialStylingValues[i + InitialStylingValuesIndex.ValueOffset];
|
||||
export function renderInitialStyles(
|
||||
element: RElement, context: StylingContext, renderer: Renderer3, startIndex?: number) {
|
||||
const initialStyles = context[StylingIndex.InitialStyleValuesPosition];
|
||||
let i = startIndex || InitialStylingValuesIndex.KeyValueStartPosition;
|
||||
while (i < initialStyles.length) {
|
||||
const value = initialStyles[i + InitialStylingValuesIndex.ValueOffset];
|
||||
if (value) {
|
||||
if (isEntryClassBased) {
|
||||
setClass(
|
||||
element, initialStylingValues[i + InitialStylingValuesIndex.PropOffset] as string, true,
|
||||
renderer, null);
|
||||
} else {
|
||||
setStyle(
|
||||
element, initialStylingValues[i + InitialStylingValuesIndex.PropOffset] as string,
|
||||
element, initialStyles[i + InitialStylingValuesIndex.PropOffset] as string,
|
||||
value as string, renderer, null);
|
||||
}
|
||||
i += InitialStylingValuesIndex.Size;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
export function allowNewBindingsForStylingContext(context: StylingContext): boolean {
|
||||
|
@ -375,7 +381,7 @@ export function updateContextWithBindings(
|
|||
}
|
||||
|
||||
// if a property is not found in the initial style values list then it
|
||||
// is ALWAYS added incase a follow-up directive introduces the same initial
|
||||
// is ALWAYS added in case a follow-up directive introduces the same initial
|
||||
// style/class value later on.
|
||||
let initialValuesToLookup = entryIsClassBased ? initialClasses : initialStyles;
|
||||
let indexForInitial = getInitialStylingValuesIndexOf(initialValuesToLookup, propName);
|
||||
|
@ -765,7 +771,7 @@ function patchStylingMapIntoContext(
|
|||
const mapProp = props[i];
|
||||
|
||||
if (!mapProp) {
|
||||
// this is an early exit incase a value was already encountered above in the
|
||||
// this is an early exit in case a value was already encountered above in the
|
||||
// previous loop (which means that the property was applied or rejected)
|
||||
continue;
|
||||
}
|
||||
|
@ -801,7 +807,7 @@ function patchStylingMapIntoContext(
|
|||
// SKIP IF INITIAL CHECK
|
||||
// If the former `value` is `null` then it means that an initial value
|
||||
// could be being rendered on screen. If that is the case then there is
|
||||
// no point in updating the value incase it matches. In other words if the
|
||||
// no point in updating the value in case it matches. In other words if the
|
||||
// new value is the exact same as the previously rendered value (which
|
||||
// happens to be the initial value) then do nothing.
|
||||
if (distantCtxValue !== null ||
|
||||
|
@ -872,7 +878,7 @@ function patchStylingMapIntoContext(
|
|||
// reapply their values into the object. For this to happen, the cached array needs to be updated
|
||||
// with dirty flags so that follow-up calls to `updateStylingMap` will reapply their styling code.
|
||||
// the reapplication of styling code within the context will reshape it and update the offset
|
||||
// values (also follow-up directives can write new values incase earlier directives set anything
|
||||
// values (also follow-up directives can write new values in case earlier directives set anything
|
||||
// to null due to removals or falsy values).
|
||||
valuesEntryShapeChange = valuesEntryShapeChange || existingCachedValueCount !== totalUniqueValues;
|
||||
updateCachedMapValue(
|
||||
|
@ -1074,7 +1080,7 @@ export function renderStyling(
|
|||
|
||||
// VALUE DEFER CASE 2: Use the initial value if all else fails (is falsy)
|
||||
// the initial value will always be a string or null,
|
||||
// therefore we can safely adopt it incase there's nothing else
|
||||
// therefore we can safely adopt it in case there's nothing else
|
||||
// note that this should always be a falsy check since `false` is used
|
||||
// for both class and style comparisons (styles can't be false and false
|
||||
// classes are turned off and should therefore defer to their initial values)
|
||||
|
@ -1742,7 +1748,7 @@ function allowValueChange(
|
|||
// directive is the parent component directive (the template) and each directive
|
||||
// that is added after is considered less important than the previous entry. This
|
||||
// prioritization of directives enables the styling algorithm to decide if a style
|
||||
// or class should be allowed to be updated/replaced incase an earlier directive
|
||||
// or class should be allowed to be updated/replaced in case an earlier directive
|
||||
// already wrote to the exact same style-property or className value. In other words
|
||||
// this decides what to do if and when there is a collision.
|
||||
if (currentValue != null) {
|
||||
|
@ -1751,7 +1757,7 @@ function allowValueChange(
|
|||
// previous directive's value...
|
||||
return newDirectiveOwner <= currentDirectiveOwner;
|
||||
} else {
|
||||
// only write a null value incase it's the same owner writing it.
|
||||
// only write a null value in case it's the same owner writing it.
|
||||
// this avoids having a higher-priority directive write to null
|
||||
// only to have a lesser-priority directive change right to a
|
||||
// non-null value immediately afterwards.
|
||||
|
@ -1955,7 +1961,7 @@ function registerMultiMapEntry(
|
|||
while (cachedValues.length < limit) {
|
||||
// this means that ONLY directive class styling (like ngClass) was used
|
||||
// therefore the root directive will still need to be filled in as well
|
||||
// as any other directive spaces incase they only used static values
|
||||
// as any other directive spaces in case they only used static values
|
||||
cachedValues.push(0, startPosition, null, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 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 {Component, HostBinding} from '@angular/core';
|
||||
import {Component, Directive, ElementRef, HostBinding} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
|
||||
|
@ -96,4 +96,61 @@ describe('acceptance integration tests', () => {
|
|||
expect(element.classList.contains('foo')).toBeTruthy();
|
||||
expect(element.classList.contains('baz')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render inline style and class attribute values on the element before a directive is instantiated',
|
||||
() => {
|
||||
@Component({
|
||||
template: `
|
||||
<div directive-expecting-styling style="width:200px" class="abc xyz"></div>
|
||||
`
|
||||
})
|
||||
class Cmp {
|
||||
}
|
||||
|
||||
@Directive({selector: '[directive-expecting-styling]'})
|
||||
class DirectiveExpectingStyling {
|
||||
constructor(elm: ElementRef) {
|
||||
const native = elm.nativeElement;
|
||||
native.setAttribute('data-captured-width', native.style.width);
|
||||
native.setAttribute('data-captured-classes', native.className);
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp, DirectiveExpectingStyling]});
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement.querySelector('div');
|
||||
expect(element.style.width).toEqual('200px');
|
||||
expect(element.getAttribute('data-captured-width')).toEqual('200px');
|
||||
expect(element.className.trim()).toEqual('abc xyz');
|
||||
expect(element.getAttribute('data-captured-classes')).toEqual('abc xyz');
|
||||
});
|
||||
|
||||
it('should only render the same initial styling values once before a directive runs', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<div directive-expecting-styling style="width:200px" class="abc"></div>
|
||||
`
|
||||
})
|
||||
class Cmp {
|
||||
}
|
||||
|
||||
@Directive({selector: '[directive-expecting-styling]'})
|
||||
class DirectiveExpectingStyling {
|
||||
constructor(elm: ElementRef) {
|
||||
const native = elm.nativeElement;
|
||||
native.style.width = '300px';
|
||||
native.classList.remove('abc');
|
||||
}
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp, DirectiveExpectingStyling]});
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement.querySelector('div');
|
||||
expect(element.style.width).toEqual('300px');
|
||||
expect(element.classList.contains('abc')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -605,9 +605,6 @@
|
|||
{
|
||||
"name": "renderInitialStyles"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStylingValues"
|
||||
},
|
||||
{
|
||||
"name": "renderStringify"
|
||||
},
|
||||
|
|
|
@ -425,9 +425,6 @@
|
|||
{
|
||||
"name": "renderInitialStyles"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStylingValues"
|
||||
},
|
||||
{
|
||||
"name": "renderStringify"
|
||||
},
|
||||
|
|
|
@ -1178,9 +1178,6 @@
|
|||
{
|
||||
"name": "renderInitialStyles"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStylingValues"
|
||||
},
|
||||
{
|
||||
"name": "renderStringify"
|
||||
},
|
||||
|
|
|
@ -104,14 +104,6 @@ window.testBlocklist = {
|
|||
"MatSnackBar with TemplateRef should be able to open a snack bar using a TemplateRef": {
|
||||
"error": "Error: Expected ' Fries Pizza ' to contain 'Pasta'.",
|
||||
"notes": "Breaking change: Change detection follows insertion tree only, not declaration tree (MatSnackBarContainer is OnPush)"
|
||||
},
|
||||
"MatTooltip special cases should clear the `user-select` when a tooltip is set on a text field": {
|
||||
"error": "Error: Expected 'none' to be falsy.",
|
||||
"notes": "FW-1133: Inline styles are not applied before constructor is run"
|
||||
},
|
||||
"MatTooltip special cases should clear the `-webkit-user-drag` on draggable elements": {
|
||||
"error": "Error: Expected 'none' to be falsy.",
|
||||
"notes": "FW-1133: Inline styles are not applied before constructor is run"
|
||||
}
|
||||
};
|
||||
// clang-format on
|
Loading…
Reference in New Issue