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:
parent
b41da03f00
commit
627cecdfe2
|
@ -12,7 +12,7 @@
|
|||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime": 1440,
|
||||
"main": 12708,
|
||||
"main": 12885,
|
||||
"polyfills": 38390
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -139,3 +139,8 @@ hr {
|
|||
left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.border {
|
||||
outline:2px solid maroon;
|
||||
display:block;
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -131,6 +131,9 @@
|
|||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
{
|
||||
"name": "applyOnCreateInstructions"
|
||||
},
|
||||
{
|
||||
"name": "attachPatchData"
|
||||
},
|
||||
|
@ -320,6 +323,9 @@
|
|||
{
|
||||
"name": "invertObject"
|
||||
},
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isComponentDef"
|
||||
},
|
||||
|
|
|
@ -386,6 +386,9 @@
|
|||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
{
|
||||
"name": "applyOnCreateInstructions"
|
||||
},
|
||||
{
|
||||
"name": "assertTemplate"
|
||||
},
|
||||
|
@ -911,6 +914,9 @@
|
|||
{
|
||||
"name": "invokeDirectivesHostBindings"
|
||||
},
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAnimationProp"
|
||||
},
|
||||
|
|
|
@ -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 || [])]: []
|
||||
|
|
Loading…
Reference in New Issue