fix(ivy): ensure host bindings and host styling works on a root component (#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 #28664
This commit is contained in:
Matias Niemelä 2019-02-12 12:04:44 -08:00 committed by Miško Hevery
parent b41da03f00
commit 627cecdfe2
13 changed files with 134 additions and 45 deletions

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 12708,
"main": 12885,
"polyfills": 38390
}
}

View File

@ -18,14 +18,14 @@ import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
import {publishDefaultGlobalUtils} from './global_utils';
import {registerPostOrderHooks, registerPreOrderHooks} from './hooks';
import {addToViewTree, CLEAN_PROMISE, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews,} from './instructions';
import {CLEAN_PROMISE, addToViewTree, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions';
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
import {PlayerHandler} from './interfaces/player';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer';
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, RootContext, RootContextFlags, TVIEW, T_HOST} from './interfaces/view';
import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state';
import {defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util';
import {applyOnCreateInstructions, defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util';
@ -200,9 +200,10 @@ export function createRootComponent<T>(
if (tView.firstTemplatePass && componentDef.hostBindings) {
const rootTNode = getPreviousOrParentTNode();
setCurrentDirectiveDef(componentDef);
componentDef.hostBindings(RenderFlags.Create, component, rootTNode.index - HEADER_OFFSET);
setCurrentDirectiveDef(null);
const expando = tView.expandoInstructions !;
invokeHostBindingsInCreationMode(
componentDef, expando, component, rootTNode, tView.firstTemplatePass);
rootTNode.onElementCreationFns && applyOnCreateInstructions(rootTNode);
}
return component;

View File

@ -32,7 +32,7 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'
import {LQueries} from './interfaces/query';
import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization';
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';
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';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
@ -41,7 +41,7 @@ import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticCo
import {BoundPlayerFactory} from './styling/player_factory';
import {ANIMATION_PROP_PREFIX, allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput, hasStyling, isAnimationProp} from './styling/util';
import {NO_CHANGE} from './tokens';
import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, isRootView, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util';
import {INTERPOLATION_DELIMITER, applyOnCreateInstructions, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, isRootView, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util';
@ -1079,18 +1079,9 @@ export function elementEnd(): void {
setPreviousOrParentTNode(previousOrParentTNode);
}
// there may be some instructions that need to run in a specific
// order because the CREATE block in a directive runs before the
// CREATE block in a template. To work around this instructions
// can get access to the function array below and defer any code
// to run after the element is created.
let fns: Function[]|null;
if (fns = previousOrParentTNode.onElementCreationFns) {
for (let i = 0; i < fns.length; i++) {
fns[i]();
}
previousOrParentTNode.onElementCreationFns = null;
}
// this is required for all host-level styling-related instructions to run
// in the correct order
previousOrParentTNode.onElementCreationFns && applyOnCreateInstructions(previousOrParentTNode);
ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Element);
const lView = getLView();
@ -1815,6 +1806,16 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
const def = tView.data[i] as DirectiveDef<any>;
const directive = viewData[i];
if (def.hostBindings) {
invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstTemplatePass);
} else if (firstTemplatePass) {
expando.push(null);
}
}
}
export function invokeHostBindingsInCreationMode(
def: DirectiveDef<any>, expando: ExpandoInstructions, directive: any, tNode: TNode,
firstTemplatePass: boolean) {
const previousExpandoLength = expando.length;
setCurrentDirectiveDef(def);
def.hostBindings !(RenderFlags.Create, directive, tNode.index - HEADER_OFFSET);
@ -1826,10 +1827,6 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
if (previousExpandoLength === expando.length && firstTemplatePass) {
expando.push(def.hostBindings);
}
} else if (firstTemplatePass) {
expando.push(null);
}
}
}
/**

View File

@ -292,6 +292,13 @@ export const enum InitPhaseState {
InitPhaseCompleted = 0b11,
}
/**
* Set of instructions used to process host bindings efficiently.
*
* See VIEW_DATA.md for more information.
*/
export interface ExpandoInstructions extends Array<number|HostBindingsFunction<any>|null> {}
/**
* The static data for an LView (shared between all templates of a
* given type).
@ -401,7 +408,7 @@ export interface TView {
*
* See VIEW_DATA.md for more information.
*/
expandoInstructions: (number|HostBindingsFunction<any>|null)[]|null;
expandoInstructions: ExpandoInstructions|null;
/**
* Full registry of directives and components that may be found in this view.

View File

@ -14,7 +14,7 @@ import {LContext} from '../interfaces/context';
import {AttributeMarker, TAttributes, TNode, TNodeFlags} from '../interfaces/node';
import {PlayState, Player, PlayerContext, PlayerIndex} from '../interfaces/player';
import {RElement} from '../interfaces/renderer';
import {InitialStylingValues, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
import {InitialStylingValues, InitialStylingValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
import {HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view';
import {getTNode} from '../util';
@ -100,10 +100,14 @@ export function getStylingContext(index: number, viewData: LView): StylingContex
}
}
export function isStylingContext(value: any): value is StylingContext {
export function isStylingContext(value: any): boolean {
// Not an LView or an LContainer
return Array.isArray(value) && typeof value[StylingIndex.MasterFlagPosition] === 'number' &&
value.length !== LCONTAINER_LENGTH;
if (Array.isArray(value) && value.length >= StylingIndex.SingleStylesStartPosition) {
return typeof value[StylingIndex.MasterFlagPosition] === 'number' &&
value[StylingIndex.InitialClassValuesPosition]
[InitialStylingValuesIndex.DefaultNullValuePosition] === null;
}
return false;
}
export function isAnimationProp(name: string): boolean {

View File

@ -317,3 +317,18 @@ export const INTERPOLATION_DELIMITER = `<60>`;
export function isPropMetadataString(str: string): boolean {
return str.indexOf(INTERPOLATION_DELIMITER) >= 0;
}
export function applyOnCreateInstructions(tNode: TNode) {
// there may be some instructions that need to run in a specific
// order because the CREATE block in a directive runs before the
// CREATE block in a template. To work around this instructions
// can get access to the function array below and defer any code
// to run after the element is created.
let fns: Function[]|null;
if (fns = tNode.onElementCreationFns) {
for (let i = 0; i < fns.length; i++) {
fns[i]();
}
tNode.onElementCreationFns = null;
}
}

View File

@ -0,0 +1,43 @@
/**
* @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 {Component, HostBinding} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
describe('acceptance integration tests', () => {
onlyInIvy('[style] and [class] bindings are a new feature')
.it('should render host bindings on the root component', () => {
@Component({template: '...'})
class MyApp {
@HostBinding('style') public myStylesExp = {};
@HostBinding('class') public myClassesExp = {};
}
TestBed.configureTestingModule({declarations: [MyApp]});
const fixture = TestBed.createComponent(MyApp);
const element = fixture.nativeElement;
fixture.detectChanges();
const component = fixture.componentInstance;
component.myStylesExp = {width: '100px'};
component.myClassesExp = 'foo';
fixture.detectChanges();
expect(element.style['width']).toEqual('100px');
expect(element.classList.contains('foo')).toBeTruthy();
component.myStylesExp = {width: '200px'};
component.myClassesExp = 'bar';
fixture.detectChanges();
expect(element.style['width']).toEqual('200px');
expect(element.classList.contains('foo')).toBeFalsy();
expect(element.classList.contains('bar')).toBeTruthy();
});
});

View File

@ -139,3 +139,8 @@ hr {
left: 8px;
}
}
.border {
outline:2px solid maroon;
display:block;
}

View File

@ -91,11 +91,13 @@ class BoxWithOverriddenStylesComponent {
<box-with-overridden-styles
style="display:block"
[style]="{'border-radius':'50px', 'border': '50px solid teal'}" [ngStyle]="{transform:'rotate(50deg)'}">
[style]="{'border-radius':'50px', 'border': '50px solid teal'}">
</box-with-overridden-styles>
`,
})
class AnimationWorldComponent {
@HostBinding('class') classVal = 'border';
items: any[] = [
{value: 1, active: false}, {value: 2, active: false}, {value: 3, active: false},
{value: 4, active: false}, {value: 5, active: false}, {value: 6, active: false},

View File

@ -56,9 +56,6 @@
{
"name": "INJECTOR_BLOOM_PARENT_SIZE"
},
{
"name": "LCONTAINER_LENGTH"
},
{
"name": "MONKEY_PATCH_KEY_NAME"
},
@ -170,6 +167,9 @@
{
"name": "appendChild"
},
{
"name": "applyOnCreateInstructions"
},
{
"name": "attachPatchData"
},
@ -449,6 +449,9 @@
{
"name": "invokeDirectivesHostBindings"
},
{
"name": "invokeHostBindingsInCreationMode"
},
{
"name": "isAnimationProp"
},

View File

@ -131,6 +131,9 @@
{
"name": "appendChild"
},
{
"name": "applyOnCreateInstructions"
},
{
"name": "attachPatchData"
},
@ -320,6 +323,9 @@
{
"name": "invertObject"
},
{
"name": "invokeHostBindingsInCreationMode"
},
{
"name": "isComponentDef"
},

View File

@ -386,6 +386,9 @@
{
"name": "appendChild"
},
{
"name": "applyOnCreateInstructions"
},
{
"name": "assertTemplate"
},
@ -911,6 +914,9 @@
{
"name": "invokeDirectivesHostBindings"
},
{
"name": "invokeHostBindingsInCreationMode"
},
{
"name": "isAnimationProp"
},

View File

@ -28,7 +28,7 @@ import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition'
import {NG_ELEMENT_ID} from '../../src/render3/fields';
import {ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, ProvidersFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index';
import {renderTemplate} from '../../src/render3/instructions';
import {DirectiveDefList, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeTypesOrFactory} from '../../src/render3/interfaces/definition';
import {DirectiveDefList, DirectiveTypesOrFactory, HostBindingsFunction, PipeDef, PipeDefList, PipeTypesOrFactory} from '../../src/render3/interfaces/definition';
import {PlayerHandler} from '../../src/render3/interfaces/player';
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, RendererStyleFlags3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {HEADER_OFFSET, LView} from '../../src/render3/interfaces/view';
@ -314,7 +314,7 @@ export function createComponent(
name: string, template: ComponentTemplate<any>, consts: number = 0, vars: number = 0,
directives: DirectiveTypesOrFactory = [], pipes: PipeTypesOrFactory = [],
viewQuery: ComponentTemplate<any>| null = null, providers: Provider[] = [],
viewProviders: Provider[] = []): ComponentType<any> {
viewProviders: Provider[] = [], hostBindings?: HostBindingsFunction<any>): ComponentType<any> {
return class Component {
value: any;
static ngComponentDef = defineComponent({
@ -325,7 +325,7 @@ export function createComponent(
factory: () => new Component,
template: template,
viewQuery: viewQuery,
directives: directives,
directives: directives, hostBindings,
pipes: pipes,
features: (providers.length > 0 || viewProviders.length > 0)?
[ProvidersFeature(providers || [], viewProviders || [])]: []