Skip to content

Commit 627cecd

Browse files
matskomhevery
authored andcommitted
fix(ivy): ensure host bindings and host styling works on a root component (angular#28664)
Prior to this fix if a root component was instantiated it create host bindings, but never render them once update mode ran unless one or more slot-allocated bindings were issued. Since styling in Ivy does not make use of LView slots, the host bindings function never ran on the root component. This fix ensures that the `hostBindings` function does run for a root component and also renders the schedlued styling instructions when executed. Jira Issue: FW-1062 PR Close angular#28664
1 parent b41da03 commit 627cecd

File tree

13 files changed

+134
-45
lines changed

13 files changed

+134
-45
lines changed

integration/_payload-limits.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"master": {
1313
"uncompressed": {
1414
"runtime": 1440,
15-
"main": 12708,
15+
"main": 12885,
1616
"polyfills": 38390
1717
}
1818
}

packages/core/src/render3/component.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import {getComponentDef} from './definition';
1818
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
1919
import {publishDefaultGlobalUtils} from './global_utils';
2020
import {registerPostOrderHooks, registerPreOrderHooks} from './hooks';
21-
import {addToViewTree, CLEAN_PROMISE, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews,} from './instructions';
21+
import {CLEAN_PROMISE, addToViewTree, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions';
2222
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
2323
import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
2424
import {PlayerHandler} from './interfaces/player';
2525
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
2626
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, RootContext, RootContextFlags, TVIEW, T_HOST} from './interfaces/view';
2727
import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state';
28-
import {defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util';
28+
import {applyOnCreateInstructions, defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util';
2929

3030

3131

@@ -200,9 +200,10 @@ export function createRootComponent<T>(
200200

201201
if (tView.firstTemplatePass && componentDef.hostBindings) {
202202
const rootTNode = getPreviousOrParentTNode();
203-
setCurrentDirectiveDef(componentDef);
204-
componentDef.hostBindings(RenderFlags.Create, component, rootTNode.index - HEADER_OFFSET);
205-
setCurrentDirectiveDef(null);
203+
const expando = tView.expandoInstructions !;
204+
invokeHostBindingsInCreationMode(
205+
componentDef, expando, component, rootTNode, tView.firstTemplatePass);
206+
rootTNode.onElementCreationFns && applyOnCreateInstructions(rootTNode);
206207
}
207208

208209
return component;

packages/core/src/render3/instructions.ts

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'
3232
import {LQueries} from './interfaces/query';
3333
import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
3434
import {SanitizerFn} from './interfaces/sanitization';
35-
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView, T_HOST} from './interfaces/view';
35+
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView, T_HOST} from './interfaces/view';
3636
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
3737
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
3838
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@@ -41,7 +41,7 @@ import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticCo
4141
import {BoundPlayerFactory} from './styling/player_factory';
4242
import {ANIMATION_PROP_PREFIX, allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput, hasStyling, isAnimationProp} from './styling/util';
4343
import {NO_CHANGE} from './tokens';
44-
import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, isRootView, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util';
44+
import {INTERPOLATION_DELIMITER, applyOnCreateInstructions, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, isRootView, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util';
4545

4646

4747

@@ -1079,18 +1079,9 @@ export function elementEnd(): void {
10791079
setPreviousOrParentTNode(previousOrParentTNode);
10801080
}
10811081

1082-
// there may be some instructions that need to run in a specific
1083-
// order because the CREATE block in a directive runs before the
1084-
// CREATE block in a template. To work around this instructions
1085-
// can get access to the function array below and defer any code
1086-
// to run after the element is created.
1087-
let fns: Function[]|null;
1088-
if (fns = previousOrParentTNode.onElementCreationFns) {
1089-
for (let i = 0; i < fns.length; i++) {
1090-
fns[i]();
1091-
}
1092-
previousOrParentTNode.onElementCreationFns = null;
1093-
}
1082+
// this is required for all host-level styling-related instructions to run
1083+
// in the correct order
1084+
previousOrParentTNode.onElementCreationFns && applyOnCreateInstructions(previousOrParentTNode);
10941085

10951086
ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Element);
10961087
const lView = getLView();
@@ -1815,23 +1806,29 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
18151806
const def = tView.data[i] as DirectiveDef<any>;
18161807
const directive = viewData[i];
18171808
if (def.hostBindings) {
1818-
const previousExpandoLength = expando.length;
1819-
setCurrentDirectiveDef(def);
1820-
def.hostBindings !(RenderFlags.Create, directive, tNode.index - HEADER_OFFSET);
1821-
setCurrentDirectiveDef(null);
1822-
// `hostBindings` function may or may not contain `allocHostVars` call
1823-
// (e.g. it may not if it only contains host listeners), so we need to check whether
1824-
// `expandoInstructions` has changed and if not - we still push `hostBindings` to
1825-
// expando block, to make sure we execute it for DI cycle
1826-
if (previousExpandoLength === expando.length && firstTemplatePass) {
1827-
expando.push(def.hostBindings);
1828-
}
1809+
invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstTemplatePass);
18291810
} else if (firstTemplatePass) {
18301811
expando.push(null);
18311812
}
18321813
}
18331814
}
18341815

1816+
export function invokeHostBindingsInCreationMode(
1817+
def: DirectiveDef<any>, expando: ExpandoInstructions, directive: any, tNode: TNode,
1818+
firstTemplatePass: boolean) {
1819+
const previousExpandoLength = expando.length;
1820+
setCurrentDirectiveDef(def);
1821+
def.hostBindings !(RenderFlags.Create, directive, tNode.index - HEADER_OFFSET);
1822+
setCurrentDirectiveDef(null);
1823+
// `hostBindings` function may or may not contain `allocHostVars` call
1824+
// (e.g. it may not if it only contains host listeners), so we need to check whether
1825+
// `expandoInstructions` has changed and if not - we still push `hostBindings` to
1826+
// expando block, to make sure we execute it for DI cycle
1827+
if (previousExpandoLength === expando.length && firstTemplatePass) {
1828+
expando.push(def.hostBindings);
1829+
}
1830+
}
1831+
18351832
/**
18361833
* Generates a new block in TView.expandoInstructions for this node.
18371834
*

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,13 @@ export const enum InitPhaseState {
292292
InitPhaseCompleted = 0b11,
293293
}
294294

295+
/**
296+
* Set of instructions used to process host bindings efficiently.
297+
*
298+
* See VIEW_DATA.md for more information.
299+
*/
300+
export interface ExpandoInstructions extends Array<number|HostBindingsFunction<any>|null> {}
301+
295302
/**
296303
* The static data for an LView (shared between all templates of a
297304
* given type).
@@ -401,7 +408,7 @@ export interface TView {
401408
*
402409
* See VIEW_DATA.md for more information.
403410
*/
404-
expandoInstructions: (number|HostBindingsFunction<any>|null)[]|null;
411+
expandoInstructions: ExpandoInstructions|null;
405412

406413
/**
407414
* Full registry of directives and components that may be found in this view.

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {LContext} from '../interfaces/context';
1414
import {AttributeMarker, TAttributes, TNode, TNodeFlags} from '../interfaces/node';
1515
import {PlayState, Player, PlayerContext, PlayerIndex} from '../interfaces/player';
1616
import {RElement} from '../interfaces/renderer';
17-
import {InitialStylingValues, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
17+
import {InitialStylingValues, InitialStylingValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
1818
import {HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view';
1919
import {getTNode} from '../util';
2020

@@ -100,10 +100,14 @@ export function getStylingContext(index: number, viewData: LView): StylingContex
100100
}
101101
}
102102

103-
export function isStylingContext(value: any): value is StylingContext {
103+
export function isStylingContext(value: any): boolean {
104104
// Not an LView or an LContainer
105-
return Array.isArray(value) && typeof value[StylingIndex.MasterFlagPosition] === 'number' &&
106-
value.length !== LCONTAINER_LENGTH;
105+
if (Array.isArray(value) && value.length >= StylingIndex.SingleStylesStartPosition) {
106+
return typeof value[StylingIndex.MasterFlagPosition] === 'number' &&
107+
value[StylingIndex.InitialClassValuesPosition]
108+
[InitialStylingValuesIndex.DefaultNullValuePosition] === null;
109+
}
110+
return false;
107111
}
108112

109113
export function isAnimationProp(name: string): boolean {

packages/core/src/render3/util.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,18 @@ export const INTERPOLATION_DELIMITER = `�`;
317317
export function isPropMetadataString(str: string): boolean {
318318
return str.indexOf(INTERPOLATION_DELIMITER) >= 0;
319319
}
320+
321+
export function applyOnCreateInstructions(tNode: TNode) {
322+
// there may be some instructions that need to run in a specific
323+
// order because the CREATE block in a directive runs before the
324+
// CREATE block in a template. To work around this instructions
325+
// can get access to the function array below and defer any code
326+
// to run after the element is created.
327+
let fns: Function[]|null;
328+
if (fns = tNode.onElementCreationFns) {
329+
for (let i = 0; i < fns.length; i++) {
330+
fns[i]();
331+
}
332+
tNode.onElementCreationFns = null;
333+
}
334+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
import {Component, HostBinding} from '@angular/core';
9+
import {TestBed} from '@angular/core/testing';
10+
import {expect} from '@angular/platform-browser/testing/src/matchers';
11+
import {onlyInIvy} from '@angular/private/testing';
12+
13+
describe('acceptance integration tests', () => {
14+
onlyInIvy('[style] and [class] bindings are a new feature')
15+
.it('should render host bindings on the root component', () => {
16+
@Component({template: '...'})
17+
class MyApp {
18+
@HostBinding('style') public myStylesExp = {};
19+
@HostBinding('class') public myClassesExp = {};
20+
}
21+
22+
TestBed.configureTestingModule({declarations: [MyApp]});
23+
const fixture = TestBed.createComponent(MyApp);
24+
const element = fixture.nativeElement;
25+
fixture.detectChanges();
26+
27+
const component = fixture.componentInstance;
28+
component.myStylesExp = {width: '100px'};
29+
component.myClassesExp = 'foo';
30+
fixture.detectChanges();
31+
32+
expect(element.style['width']).toEqual('100px');
33+
expect(element.classList.contains('foo')).toBeTruthy();
34+
35+
component.myStylesExp = {width: '200px'};
36+
component.myClassesExp = 'bar';
37+
fixture.detectChanges();
38+
39+
expect(element.style['width']).toEqual('200px');
40+
expect(element.classList.contains('foo')).toBeFalsy();
41+
expect(element.classList.contains('bar')).toBeTruthy();
42+
});
43+
});

packages/core/test/bundling/animation_world/base.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,10 @@ hr {
137137

138138
.learn-bar > .learn {
139139
left: 8px;
140-
}
141-
}
140+
}
141+
}
142+
143+
.border {
144+
outline:2px solid maroon;
145+
display:block;
146+
}

packages/core/test/bundling/animation_world/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,13 @@ class BoxWithOverriddenStylesComponent {
9191
9292
<box-with-overridden-styles
9393
style="display:block"
94-
[style]="{'border-radius':'50px', 'border': '50px solid teal'}" [ngStyle]="{transform:'rotate(50deg)'}">
94+
[style]="{'border-radius':'50px', 'border': '50px solid teal'}">
9595
</box-with-overridden-styles>
9696
`,
9797
})
9898
class AnimationWorldComponent {
99+
@HostBinding('class') classVal = 'border';
100+
99101
items: any[] = [
100102
{value: 1, active: false}, {value: 2, active: false}, {value: 3, active: false},
101103
{value: 4, active: false}, {value: 5, active: false}, {value: 6, active: false},

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,6 @@
5656
{
5757
"name": "INJECTOR_BLOOM_PARENT_SIZE"
5858
},
59-
{
60-
"name": "LCONTAINER_LENGTH"
61-
},
6259
{
6360
"name": "MONKEY_PATCH_KEY_NAME"
6461
},
@@ -170,6 +167,9 @@
170167
{
171168
"name": "appendChild"
172169
},
170+
{
171+
"name": "applyOnCreateInstructions"
172+
},
173173
{
174174
"name": "attachPatchData"
175175
},
@@ -449,6 +449,9 @@
449449
{
450450
"name": "invokeDirectivesHostBindings"
451451
},
452+
{
453+
"name": "invokeHostBindingsInCreationMode"
454+
},
452455
{
453456
"name": "isAnimationProp"
454457
},

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@
131131
{
132132
"name": "appendChild"
133133
},
134+
{
135+
"name": "applyOnCreateInstructions"
136+
},
134137
{
135138
"name": "attachPatchData"
136139
},
@@ -320,6 +323,9 @@
320323
{
321324
"name": "invertObject"
322325
},
326+
{
327+
"name": "invokeHostBindingsInCreationMode"
328+
},
323329
{
324330
"name": "isComponentDef"
325331
},

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,9 @@
386386
{
387387
"name": "appendChild"
388388
},
389+
{
390+
"name": "applyOnCreateInstructions"
391+
},
389392
{
390393
"name": "assertTemplate"
391394
},
@@ -911,6 +914,9 @@
911914
{
912915
"name": "invokeDirectivesHostBindings"
913916
},
917+
{
918+
"name": "invokeHostBindingsInCreationMode"
919+
},
914920
{
915921
"name": "isAnimationProp"
916922
},

packages/core/test/render3/render_util.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition'
2828
import {NG_ELEMENT_ID} from '../../src/render3/fields';
2929
import {ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, ProvidersFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index';
3030
import {renderTemplate} from '../../src/render3/instructions';
31-
import {DirectiveDefList, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeTypesOrFactory} from '../../src/render3/interfaces/definition';
31+
import {DirectiveDefList, DirectiveTypesOrFactory, HostBindingsFunction, PipeDef, PipeDefList, PipeTypesOrFactory} from '../../src/render3/interfaces/definition';
3232
import {PlayerHandler} from '../../src/render3/interfaces/player';
3333
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, RendererStyleFlags3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
3434
import {HEADER_OFFSET, LView} from '../../src/render3/interfaces/view';
@@ -314,7 +314,7 @@ export function createComponent(
314314
name: string, template: ComponentTemplate<any>, consts: number = 0, vars: number = 0,
315315
directives: DirectiveTypesOrFactory = [], pipes: PipeTypesOrFactory = [],
316316
viewQuery: ComponentTemplate<any>| null = null, providers: Provider[] = [],
317-
viewProviders: Provider[] = []): ComponentType<any> {
317+
viewProviders: Provider[] = [], hostBindings?: HostBindingsFunction<any>): ComponentType<any> {
318318
return class Component {
319319
value: any;
320320
static ngComponentDef = defineComponent({
@@ -325,7 +325,7 @@ export function createComponent(
325325
factory: () => new Component,
326326
template: template,
327327
viewQuery: viewQuery,
328-
directives: directives,
328+
directives: directives, hostBindings,
329329
pipes: pipes,
330330
features: (providers.length > 0 || viewProviders.length > 0)?
331331
[ProvidersFeature(providers || [], viewProviders || [])]: []

0 commit comments

Comments
 (0)