Skip to content

Commit 5aabe93

Browse files
committed
refactor(ivy): Switch styling to new reconcile algorithm (angular#34616)
NOTE: This change must be reverted with previous deletes so that it code remains in build-able state. This change deletes old styling code and replaces it with a simplified styling algorithm. The mental model for the new algorithm is: - Create a linked list of styling bindings in the order of priority. All styling bindings ere executed in compiled order and than a linked list of bindings is created in priority order. - Flush the style bindings at the end of `advance()` instruction. This implies that there are two flush events. One at the end of template `advance` instruction in the template. Second one at the end of `hostBindings` `advance` instruction when processing host bindings (if any). - Each binding instructions effectively updates the string to represent the string at that location. Because most of the bindings are additive, this is a cheap strategy in most cases. In rare cases the strategy requires removing tokens from the styling up to this point. (We expect that to be rare case)S Because, the bindings are presorted in the order of priority, it is safe to resume the processing of the concatenated string from the last change binding. PR Close angular#34616
1 parent b4a711e commit 5aabe93

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2419
-1393
lines changed

aio/scripts/_payload-limits.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"master": {
1313
"uncompressed": {
1414
"runtime-es2015": 2987,
15-
"main-es2015": 462235,
15+
"main-es2015": 456890,
1616
"polyfills-es2015": 52503
1717
}
1818
}
@@ -21,7 +21,7 @@
2121
"master": {
2222
"uncompressed": {
2323
"runtime-es2015": 3097,
24-
"main-es2015": 438671,
24+
"main-es2015": 425216,
2525
"polyfills-es2015": 52503
2626
}
2727
}

integration/_payload-limits.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"master": {
1313
"uncompressed": {
1414
"runtime-es2015": 1485,
15-
"main-es2015": 18214,
15+
"main-es2015": 16787,
1616
"polyfills-es2015": 36808
1717
}
1818
}
@@ -30,7 +30,7 @@
3030
"master": {
3131
"uncompressed": {
3232
"runtime-es2015": 1485,
33-
"main-es2015": 139487,
33+
"main-es2015": 137226,
3434
"polyfills-es2015": 37494
3535
}
3636
}
@@ -39,7 +39,7 @@
3939
"master": {
4040
"uncompressed": {
4141
"runtime-es2015": 2289,
42-
"main-es2015": 268796,
42+
"main-es2015": 254857,
4343
"polyfills-es2015": 36808,
4444
"5-es2015": 751
4545
}
@@ -49,7 +49,7 @@
4949
"master": {
5050
"uncompressed": {
5151
"runtime-es2015": 2289,
52-
"main-es2015": 228770,
52+
"main-es2015": 226519,
5353
"polyfills-es2015": 36808,
5454
"5-es2015": 779
5555
}
@@ -60,7 +60,7 @@
6060
"uncompressed": {
6161
"bundle": "TODO(i): temporarily increase the payload size limit from 105779 - this is due to a closure issue related to ESM reexports that still needs to be investigated",
6262
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
63-
"bundle": 176433
63+
"bundle": 175498
6464
}
6565
}
6666
}

modules/benchmarks/src/styling/ng2/styling.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {BrowserModule} from '@angular/platform-browser';
1818
<ng-template #t3><button [class.bar]="exp ==='bar'"></button></ng-template>
1919
<ng-template #t4><button class="foo" [class.bar]="exp ==='bar'"></button></ng-template>
2020
<ng-template #t5><button class="foo" [ngClass]="{bar: exp ==='bar'}"></button></ng-template>
21-
<ng-template #t6><button class="foo" [ngStyle]="staticStyle" [style.background-color]="exp"></button></ng-template>
21+
<ng-template #t6><button class="foo" [ngStyle]="staticStyle" [style.background-color]="exp == 'bar' ? 'yellow': 'red'"></button></ng-template>
2222
<ng-template #t7><button style="color: red"></button></ng-template>
2323
<ng-template #t8><button [style.width.px]="exp ==='bar' ? 10 : 20" [style.color]="exp"></button></ng-template>
2424
<ng-template #t9><button style="width: 10px" [style.color]="exp"></button></ng-template>

modules/benchmarks/src/tree/render3_function/index.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class TreeFunction {
2525
type: TreeFunction,
2626
selectors: [['tree']],
2727
decls: 5,
28-
vars: 1,
28+
vars: 2,
2929
template: function(rf: ɵRenderFlags, ctx: TreeFunction) {
3030
// bit of a hack
3131
TreeTpl(rf, ctx.data);
@@ -34,6 +34,7 @@ export class TreeFunction {
3434
});
3535
}
3636

37+
const TreeFunctionCmpDef = TreeFunction.ɵcmp as{decls: number, vars: number};
3738
export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
3839
if (rf & ɵRenderFlags.Create) {
3940
ɵɵelementStart(0, 'tree');
@@ -54,7 +55,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
5455
ɵɵcontainerRefreshStart(3);
5556
{
5657
if (ctx.left != null) {
57-
let rf0 = ɵɵembeddedViewStart(0, 5, 1);
58+
let rf0 = ɵɵembeddedViewStart(0, 5, 2);
5859
{ TreeTpl(rf0, ctx.left); }
5960
ɵɵembeddedViewEnd();
6061
}
@@ -63,7 +64,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
6364
ɵɵcontainerRefreshStart(4);
6465
{
6566
if (ctx.right != null) {
66-
let rf0 = ɵɵembeddedViewStart(0, 5, 1);
67+
let rf0 = ɵɵembeddedViewStart(0, TreeFunctionCmpDef.decls, TreeFunctionCmpDef.vars);
6768
{ TreeTpl(rf0, ctx.right); }
6869
ɵɵembeddedViewEnd();
6970
}

packages/common/src/directives/ng_class.ts

+7
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,11 @@ export class NgClass implements DoCheck {
157157
});
158158
}
159159
}
160+
161+
// TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs
162+
// We need to export this to make angular/flex-layout happy
163+
// https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9
164+
setClass(value: string) { this.klass = value; }
165+
setNgClass(value: any) { this.ngClass = value; }
166+
applyChanges() { this.ngDoCheck(); }
160167
}

packages/common/src/directives/ng_style.ts

+6
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,10 @@ export class NgStyle implements DoCheck {
8585
changes.forEachAddedItem((record) => this._setStyle(record.key, record.currentValue));
8686
changes.forEachChangedItem((record) => this._setStyle(record.key, record.currentValue));
8787
}
88+
89+
// TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs
90+
// We need to export this to make angular/flex-layout happy
91+
// https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9
92+
setNgStyle(value: any) { this.ngStyle = value; }
93+
applyChanges() { this.ngDoCheck(); }
8894
}

packages/common/src/private_export.ts

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

9+
// TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs
10+
// We need to export this to make angular/flex-layout happy
11+
// https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9
12+
export {NgClass as ɵNgClassImpl, NgClass as ɵNgClassR2Impl} from './directives/ng_class';
13+
export {NgStyle as ɵNgStyleR2Impl} from './directives/ng_style';
14+
915
export {DomAdapter as ɵDomAdapter, getDOM as ɵgetDOM, setRootDomAdapter as ɵsetRootDomAdapter} from './dom_adapter';
1016
export {BrowserPlatformLocation as ɵBrowserPlatformLocation} from './location/platform_location';

packages/common/test/directives/ng_class_spec.ts

+23-3
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing';
196196
fixture = createTestComponent(`<div [ngClass]="['foo', {}]"></div>`);
197197
expect(() => fixture !.detectChanges())
198198
.toThrowError(
199-
/NgClass can only toggle CSS classes expressed as strings, got: \[object Object\]/);
199+
/NgClass can only toggle CSS classes expressed as strings, got \[object Object\]/);
200200
});
201201
});
202202

@@ -372,15 +372,35 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing';
372372
detectChangesAndExpectClassName('color-red');
373373
});
374374

375+
it('should allow classes with trailing and leading spaces in [ngClass]', () => {
376+
@Component({
377+
template: `
378+
<div leading-space [ngClass]="{' foo': applyClasses}"></div>
379+
<div trailing-space [ngClass]="{'foo ': applyClasses}"></div>
380+
`
381+
})
382+
class Cmp {
383+
applyClasses = true;
384+
}
385+
386+
TestBed.configureTestingModule({declarations: [Cmp]});
387+
const fixture = TestBed.createComponent(Cmp);
388+
fixture.detectChanges();
389+
390+
const leading = fixture.nativeElement.querySelector('[leading-space]');
391+
const trailing = fixture.nativeElement.querySelector('[trailing-space]');
392+
expect(leading.className).toBe('foo');
393+
expect(trailing.className).toBe('foo');
394+
});
395+
375396
});
376397
});
377398
}
378399

379400
@Component({selector: 'test-cmp', template: ''})
380401
class TestComponent {
381402
condition: boolean = true;
382-
// TODO(issue/24571): remove '!'.
383-
items !: any[];
403+
items: any[]|undefined;
384404
arrExpr: string[] = ['foo'];
385405
setExpr: Set<string> = new Set<string>();
386406
objExpr: {[klass: string]: any}|null = {'foo': true, 'bar': false};

packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ describe('compiler compliance: bindings', () => {
856856
type: HostAttributeDir,
857857
selectors: [["", "hostAttributeDir", ""]],
858858
hostAttrs: ["title", "hello there from directive", ${AttributeMarker.Classes}, "one", "two", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"],
859-
hostVars: 2,
859+
hostVars: 4,
860860
hostBindings: function HostAttributeDir_HostBindings(rf, ctx, elIndex) {
861861
862862
}

packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts

+8-32
Original file line numberDiff line numberDiff line change
@@ -1006,15 +1006,8 @@ describe('compiler compliance: styling', () => {
10061006

10071007
const template = `
10081008
hostAttrs: [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"],
1009-
hostVars: 6,
1009+
hostVars: 8,
10101010
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
1011-
<<<<<<< HEAD
1012-
=======
1013-
if (rf & 1) {
1014-
$r3$.ɵɵallocHostVars(8);
1015-
$r3$.ɵɵelementHostAttrs($e0_attrs$);
1016-
}
1017-
>>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction
10181011
if (rf & 2) {
10191012
$r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer);
10201013
$r3$.ɵɵclassMap(ctx.myClass);
@@ -1068,14 +1061,8 @@ describe('compiler compliance: styling', () => {
10681061
};
10691062

10701063
const template = `
1071-
hostVars: 8,
1064+
hostVars: 12,
10721065
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
1073-
<<<<<<< HEAD
1074-
=======
1075-
if (rf & 1) {
1076-
$r3$.ɵɵallocHostVars(12);
1077-
}
1078-
>>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction
10791066
if (rf & 2) {
10801067
$r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer);
10811068
$r3$.ɵɵclassMap(ctx.myClasses);
@@ -1143,14 +1130,8 @@ describe('compiler compliance: styling', () => {
11431130
`;
11441131

11451132
const hostBindings = `
1146-
hostVars: 6,
1133+
hostVars: 8,
11471134
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
1148-
<<<<<<< HEAD
1149-
=======
1150-
if (rf & 1) {
1151-
$r3$.ɵɵallocHostVars(8);
1152-
}
1153-
>>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction
11541135
if (rf & 2) {
11551136
$r3$.ɵɵstyleMap(ctx.myStyleExp, $r3$.ɵɵdefaultStyleSanitizer);
11561137
$r3$.ɵɵclassMap(ctx.myClassExp);
@@ -1212,22 +1193,23 @@ describe('compiler compliance: styling', () => {
12121193
// NOTE: IF YOU ARE CHANGING THIS COMPILER SPEC, YOU MAY NEED TO CHANGE THE DIRECTIVE
12131194
// DEF THAT'S HARD-CODED IN `ng_class.ts`.
12141195
const template = `
1196+
12151197
hostVars: 2,
12161198
hostBindings: function ClassDirective_HostBindings(rf, ctx, elIndex) {
12171199
if (rf & 2) {
12181200
$r3$.ɵɵclassMap(ctx.myClassMap);
12191201
}
12201202
}
12211203
1222-
hostVars: 2,
1204+
hostVars: 4,
12231205
hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) {
12241206
if (rf & 2) {
12251207
$r3$.ɵɵstyleProp("width", ctx.myWidth);
12261208
$r3$.ɵɵclassProp("foo", ctx.myFooClass);
12271209
}
12281210
}
12291211
1230-
hostVars: 2,
1212+
hostVars: 4,
12311213
hostBindings: function HeightDirective_HostBindings(rf, ctx, elIndex) {
12321214
if (rf & 2) {
12331215
$r3$.ɵɵstyleProp("height", ctx.myHeight);
@@ -1917,7 +1899,7 @@ describe('compiler compliance: styling', () => {
19171899
};
19181900

19191901
const template = `
1920-
hostVars: 4,
1902+
hostVars: 6,
19211903
hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) {
19221904
if (rf & 2) {
19231905
$r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title);
@@ -2079,14 +2061,8 @@ describe('compiler compliance: styling', () => {
20792061
};
20802062

20812063
const template = `
2082-
hostVars: 9,
2064+
hostVars: 10,
20832065
hostBindings: function MyDir_HostBindings(rf, ctx, elIndex) {
2084-
<<<<<<< HEAD
2085-
=======
2086-
2087-
$r3$.ɵɵallocHostVars(10);
2088-
2089-
>>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction
20902066
if (rf & 2) {
20912067
$r3$.ɵɵhostProperty("title", ctx.title);
20922068
$r3$.ɵɵupdateSyntheticHostBinding("@anim",

packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2340,7 +2340,7 @@ runInEachFileSystem(os => {
23402340
env.driveMain();
23412341
const jsContents = env.getContents('test.js');
23422342
const hostBindingsFn = `
2343-
hostVars: 3,
2343+
hostVars: 4,
23442344
hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) {
23452345
if (rf & 1) {
23462346
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody)("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); });

packages/compiler/src/render3/view/styling_builder.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -524,9 +524,10 @@ function isStyleSanitizable(prop: string): boolean {
524524
// Note that browsers support both the dash case and
525525
// camel case property names when setting through JS.
526526
return prop === 'background-image' || prop === 'backgroundImage' || prop === 'background' ||
527-
prop === 'border-image' || prop === 'borderImage' || prop === 'filter' ||
528-
prop === 'list-style' || prop === 'listStyle' || prop === 'list-style-image' ||
529-
prop === 'listStyleImage' || prop === 'clip-path' || prop === 'clipPath';
527+
prop === 'border-image' || prop === 'borderImage' || prop === 'border-image-source' ||
528+
prop === 'borderImageSource' || prop === 'filter' || prop === 'list-style' ||
529+
prop === 'listStyle' || prop === 'list-style-image' || prop === 'listStyleImage' ||
530+
prop === 'clip-path' || prop === 'clipPath';
530531
}
531532

532533
/**

packages/compiler/test/render3/style_parser_spec.ts

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ describe('style parsing', () => {
2121
expect(result).toEqual(['width', '100px', 'height', '200px', 'opacity', '0']);
2222
});
2323

24+
it('should allow empty values', () => {
25+
const result = parseStyle('width:;height: ;');
26+
expect(result).toEqual(['width', '', 'height', '']);
27+
});
28+
2429
it('should trim values and properties', () => {
2530
const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;');
2631
expect(result).toEqual(['width', '333px', 'height', '666px', 'opacity', '0.5']);

packages/core/src/render3/assert.ts

+5
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ export function assertFirstCreatePass(tView: TView, errMessage?: string) {
7373
tView.firstCreatePass, true, errMessage || 'Should only be called in first create pass.');
7474
}
7575

76+
export function assertFirstUpdatePass(tView: TView, errMessage?: string) {
77+
assertEqual(
78+
tView.firstUpdatePass, true, errMessage || 'Should only be called in first update pass.');
79+
}
80+
7681
/**
7782
* This is a basic sanity check that an object is probably a directive def. DirectiveDef is
7883
* an interface, so we can't do a direct instanceof check.

packages/core/src/render3/bindings.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,19 @@ export function getBinding(lView: LView, bindingIndex: number): any {
3030
return lView[bindingIndex];
3131
}
3232

33-
/** Updates binding if changed, then returns whether it was updated. */
33+
/**
34+
* Updates binding if changed, then returns whether it was updated.
35+
*
36+
* This function also checks the `CheckNoChangesMode` and throws if changes are made.
37+
* Some changes (Objects/iterables) during `CheckNoChangesMode` are exempt to comply with VE
38+
* behavior.
39+
*
40+
* @param lView current `LView`
41+
* @param bindingIndex The binding in the `LView` to check
42+
* @param value New value to check against `lView[bindingIndex]`
43+
* @returns `true` if the bindings has changed. (Throws if binding has changed during
44+
* `CheckNoChangesMode`)
45+
*/
3446
export function bindingUpdated(lView: LView, bindingIndex: number, value: any): boolean {
3547
ngDevMode && assertNotSame(value, NO_CHANGE, 'Incoming value should never be NO_CHANGE.');
3648
ngDevMode &&
@@ -50,6 +62,11 @@ export function bindingUpdated(lView: LView, bindingIndex: number, value: any):
5062
throwErrorIfNoChangesMode(
5163
oldValue === NO_CHANGE, details.oldValue, details.newValue, details.propName);
5264
}
65+
// There was a change, but the `devModeEqual` decided that the change is exempt from an error.
66+
// For this reason we exit as if no change. The early exit is needed to prevent the changed
67+
// value to be written into `LView` (If we would write the new value that we would not see it
68+
// as change on next CD.)
69+
return false;
5370
}
5471
lView[bindingIndex] = value;
5572
return true;

0 commit comments

Comments
 (0)