diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index c39f9c51db..b93f10069f 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 12708, + "main": 12885, "polyfills": 38390 } } diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 3b794ae9b5..0078a9a772 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -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( 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; diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index fdb2291800..268aed3dc8 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -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,23 +1806,29 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod const def = tView.data[i] as DirectiveDef; const directive = viewData[i]; if (def.hostBindings) { - const previousExpandoLength = expando.length; - setCurrentDirectiveDef(def); - def.hostBindings !(RenderFlags.Create, directive, tNode.index - HEADER_OFFSET); - setCurrentDirectiveDef(null); - // `hostBindings` function may or may not contain `allocHostVars` call - // (e.g. it may not if it only contains host listeners), so we need to check whether - // `expandoInstructions` has changed and if not - we still push `hostBindings` to - // expando block, to make sure we execute it for DI cycle - if (previousExpandoLength === expando.length && firstTemplatePass) { - expando.push(def.hostBindings); - } + invokeHostBindingsInCreationMode(def, expando, directive, tNode, firstTemplatePass); } else if (firstTemplatePass) { expando.push(null); } } } +export function invokeHostBindingsInCreationMode( + def: DirectiveDef, expando: ExpandoInstructions, directive: any, tNode: TNode, + firstTemplatePass: boolean) { + const previousExpandoLength = expando.length; + setCurrentDirectiveDef(def); + def.hostBindings !(RenderFlags.Create, directive, tNode.index - HEADER_OFFSET); + setCurrentDirectiveDef(null); + // `hostBindings` function may or may not contain `allocHostVars` call + // (e.g. it may not if it only contains host listeners), so we need to check whether + // `expandoInstructions` has changed and if not - we still push `hostBindings` to + // expando block, to make sure we execute it for DI cycle + if (previousExpandoLength === expando.length && firstTemplatePass) { + expando.push(def.hostBindings); + } +} + /** * Generates a new block in TView.expandoInstructions for this node. * diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 66c1121bc2..7516848d1a 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -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|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|null)[]|null; + expandoInstructions: ExpandoInstructions|null; /** * Full registry of directives and components that may be found in this view. diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts index 4d9ea60449..b2b72800dd 100644 --- a/packages/core/src/render3/styling/util.ts +++ b/packages/core/src/render3/styling/util.ts @@ -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 { diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 4f7b2991be..fd9fd5969b 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -317,3 +317,18 @@ export const INTERPOLATION_DELIMITER = `�`; 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; + } +} \ No newline at end of file diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts new file mode 100644 index 0000000000..1ee6334991 --- /dev/null +++ b/packages/core/test/acceptance/integration_spec.ts @@ -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(); + }); +}); \ No newline at end of file diff --git a/packages/core/test/bundling/animation_world/base.css b/packages/core/test/bundling/animation_world/base.css index 9f6ac1bd74..ebd81e6170 100644 --- a/packages/core/test/bundling/animation_world/base.css +++ b/packages/core/test/bundling/animation_world/base.css @@ -137,5 +137,10 @@ hr { .learn-bar > .learn { left: 8px; - } -} \ No newline at end of file + } +} + +.border { + outline:2px solid maroon; + display:block; +} diff --git a/packages/core/test/bundling/animation_world/index.ts b/packages/core/test/bundling/animation_world/index.ts index f4b3ae5f69..0f4e892a92 100644 --- a/packages/core/test/bundling/animation_world/index.ts +++ b/packages/core/test/bundling/animation_world/index.ts @@ -91,11 +91,13 @@ class BoxWithOverriddenStylesComponent { + [style]="{'border-radius':'50px', 'border': '50px solid teal'}"> `, }) 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}, diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index ecaa77c17b..a18848355a 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -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" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index dfa14e80ed..d68730da4c 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -131,6 +131,9 @@ { "name": "appendChild" }, + { + "name": "applyOnCreateInstructions" + }, { "name": "attachPatchData" }, @@ -320,6 +323,9 @@ { "name": "invertObject" }, + { + "name": "invokeHostBindingsInCreationMode" + }, { "name": "isComponentDef" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 8b58277077..dd22242097 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -386,6 +386,9 @@ { "name": "appendChild" }, + { + "name": "applyOnCreateInstructions" + }, { "name": "assertTemplate" }, @@ -911,6 +914,9 @@ { "name": "invokeDirectivesHostBindings" }, + { + "name": "invokeHostBindingsInCreationMode" + }, { "name": "isAnimationProp" }, diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 444b9dc371..ac3b3aea76 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -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, consts: number = 0, vars: number = 0, directives: DirectiveTypesOrFactory = [], pipes: PipeTypesOrFactory = [], viewQuery: ComponentTemplate| null = null, providers: Provider[] = [], - viewProviders: Provider[] = []): ComponentType { + viewProviders: Provider[] = [], hostBindings?: HostBindingsFunction): ComponentType { 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 || [])]: []