diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index f203189377..69922a84a8 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -38,13 +38,14 @@ import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, createTextNode, findComponentView, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCreationMode, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state'; -import {createStylingContextTemplate, renderStyleAndClassBindings, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; +import {createStylingContextTemplate, renderStyleAndClassBindings, setStyle, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; import {BoundPlayerFactory} from './styling/player_factory'; import {getStylingContext} from './styling/util'; import {NO_CHANGE} from './tokens'; import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util'; + /** * A permanent marker promise which signifies that the current CD tree is * clean. @@ -1216,9 +1217,6 @@ export function elementStylingApply(index: number, directive?: {}): void { export function elementStyleProp( index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, suffix?: string, directive?: {}): void { - if (directive != undefined) - return hackImplementationOfElementStyleProp( - index, styleIndex, value, suffix, directive); // supported in next PR let valueToAdd: string|null = null; if (value !== null) { if (suffix) { @@ -1233,7 +1231,11 @@ export function elementStyleProp( valueToAdd = value as any as string; } } - updateElementStyleProp(getStylingContext(index, getLView()), styleIndex, valueToAdd); + if (directive != undefined) { + hackImplementationOfElementStyleProp(index, styleIndex, valueToAdd, suffix, directive); + } else { + updateElementStyleProp(getStylingContext(index, getLView()), styleIndex, valueToAdd); + } } /** @@ -1293,14 +1295,40 @@ function hackImplementationOfElementStyling( classDeclarations: (string | boolean | InitialStylingFlags)[] | null, styleDeclarations: (string | boolean | InitialStylingFlags)[] | null, styleSanitizer: StyleSanitizeFn | null, directive: {}): void { - const node = getNativeByTNode(getPreviousOrParentTNode(), getLView()); + const node = getNativeByTNode(getPreviousOrParentTNode(), getLView()) as RElement; ngDevMode && assertDefined(node, 'expecting parent DOM node'); const hostStylingHackMap: HostStylingHackMap = ((node as any).hostStylingHack || ((node as any).hostStylingHack = new Map())); + const squashedClassDeclarations = hackSquashDeclaration(classDeclarations); hostStylingHackMap.set(directive, { - classDeclarations: hackSquashDeclaration(classDeclarations), + classDeclarations: squashedClassDeclarations, styleDeclarations: hackSquashDeclaration(styleDeclarations), styleSanitizer }); + hackSetStaticClasses(node, squashedClassDeclarations); +} + +function hackSetStaticClasses(node: RElement, classDeclarations: (string | boolean)[]) { + // Static classes need to be set here because static classes don't generate + // elementClassProp instructions. + const lView = getLView(); + const staticClassStartIndex = + classDeclarations.indexOf(InitialStylingFlags.VALUES_MODE as any) + 1; + const renderer = lView[RENDERER]; + + for (let i = staticClassStartIndex; i < classDeclarations.length; i += 2) { + const className = classDeclarations[i] as string; + const value = classDeclarations[i + 1]; + // if value is true, then this is a static class and we should set it now. + // class bindings are set separately in elementClassProp. + if (value === true) { + if (isProceduralRenderer(renderer)) { + renderer.addClass(node, className); + } else { + const classList = (node as HTMLElement).classList; + classList.add(className); + } + } + } } function hackSquashDeclaration(declarations: (string | boolean | InitialStylingFlags)[] | null): @@ -1330,9 +1358,15 @@ function hackImplementationOfElementStylingApply(index: number, directive?: {}): } function hackImplementationOfElementStyleProp( - index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, - suffix?: string, directive?: {}): void { - throw new Error('unimplemented. Should not be needed by ViewEngine compatibility'); + index: number, styleIndex: number, value: string | null, suffix?: string, + directive?: {}): void { + const lView = getLView(); + const node = getNativeByIndex(index, lView); + ngDevMode && assertDefined(node, 'could not locate node'); + const hostStylingHack: HostStylingHack = (node as any).hostStylingHack.get(directive); + const styleName = hostStylingHack.styleDeclarations[styleIndex]; + const renderer = lView[RENDERER]; + setStyle(node, styleName, value as string, renderer, null); } function hackImplementationOfElementStylingMap( diff --git a/packages/core/src/render3/styling/class_and_style_bindings.ts b/packages/core/src/render3/styling/class_and_style_bindings.ts index cd4d687e7b..ed4acc318d 100644 --- a/packages/core/src/render3/styling/class_and_style_bindings.ts +++ b/packages/core/src/render3/styling/class_and_style_bindings.ts @@ -594,7 +594,7 @@ export function renderStyleAndClassBindings( * @param renderer * @param store an optional key/value map that will be used as a context to render styles on */ -function setStyle( +export function setStyle( native: any, prop: string, value: string | null, renderer: Renderer3, sanitizer: StyleSanitizeFn | null, store?: BindingStore | null, playerBuilder?: ClassAndStylePlayerBuilder| null) { diff --git a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json index 736978ae73..01af8add4f 100644 --- a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -785,6 +785,9 @@ { "name": "hackImplementationOfElementStylingMap" }, + { + "name": "hackSetStaticClasses" + }, { "name": "hackSquashDeclaration" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index cb17bbffcb..03ead4a7ba 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -818,6 +818,9 @@ { "name": "hackImplementationOfElementStylingApply" }, + { + "name": "hackSetStaticClasses" + }, { "name": "hackSquashDeclaration" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index 98d0676673..ecdaeab2bf 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -2000,6 +2000,9 @@ { "name": "hackImplementationOfElementStylingApply" }, + { + "name": "hackSetStaticClasses" + }, { "name": "hackSquashDeclaration" }, diff --git a/packages/core/test/render3/host_binding_spec.ts b/packages/core/test/render3/host_binding_spec.ts index 26635727bb..efc11b7fb4 100644 --- a/packages/core/test/render3/host_binding_spec.ts +++ b/packages/core/test/render3/host_binding_spec.ts @@ -9,9 +9,9 @@ import {ElementRef, EventEmitter} from '@angular/core'; import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature, NgOnChangesFeature, QueryList} from '../../src/render3/index'; -import {allocHostVars, bind, directiveInject, element, elementEnd, elementProperty, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery} from '../../src/render3/instructions'; +import {allocHostVars, bind, directiveInject, element, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery} from '../../src/render3/instructions'; import {query, queryRefresh} from '../../src/render3/query'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; +import {RenderFlags, InitialStylingFlags} from '../../src/render3/interfaces/definition'; import {pureFunction1, pureFunction2} from '../../src/render3/pure_function'; import {ComponentFixture, TemplateFixture, createComponent, createDirective} from './render_util'; @@ -978,4 +978,95 @@ describe('host bindings', () => { const hostBindingEl = fixture.hostElement.querySelector('host-binding-comp') as HTMLElement; expect(hostBindingEl.id).toEqual('after-content'); }); + + describe('styles', () => { + + it('should bind to host styles', () => { + let hostBindingDir !: HostBindingToStyles; + /** + * host: { + * '[style.width.px]': 'width' + * } + */ + class HostBindingToStyles { + width = 2; + + static ngComponentDef = defineComponent({ + type: HostBindingToStyles, + selectors: [['host-binding-to-styles']], + factory: () => hostBindingDir = new HostBindingToStyles(), + consts: 0, + vars: 0, + hostBindings: (rf: RenderFlags, ctx: HostBindingToStyles, elIndex: number) => { + if (rf & RenderFlags.Create) { + allocHostVars(0); // this is wrong, but necessary until FW-761 gets in + elementStyling(null, ['width'], null, ctx); + } + if (rf & RenderFlags.Update) { + elementStyleProp(0, 0, ctx.width, 'px', ctx); + elementStylingApply(0, ctx); + } + }, + template: (rf: RenderFlags, cmp: HostBindingToStyles) => {} + }); + } + + /** */ + const App = createComponent('app', (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + element(0, 'host-binding-to-styles'); + } + }, 1, 0, [HostBindingToStyles]); + + const fixture = new ComponentFixture(App); + const hostBindingEl = + fixture.hostElement.querySelector('host-binding-to-styles') as HTMLElement; + expect(hostBindingEl.style.width).toEqual('2px'); + + hostBindingDir.width = 5; + fixture.update(); + expect(hostBindingEl.style.width).toEqual('5px'); + }); + + it('should apply static host classes', () => { + /** + * host: { + * 'class': 'mat-toolbar' + * } + */ + class StaticHostClass { + static ngComponentDef = defineComponent({ + type: StaticHostClass, + selectors: [['static-host-class']], + factory: () => new StaticHostClass(), + consts: 0, + vars: 0, + hostBindings: (rf: RenderFlags, ctx: StaticHostClass, elIndex: number) => { + if (rf & RenderFlags.Create) { + allocHostVars(0); // this is wrong, but necessary until FW-761 gets in + elementStyling( + ['mat-toolbar', InitialStylingFlags.VALUES_MODE, 'mat-toolbar', true], null, null, + ctx); + } + if (rf & RenderFlags.Update) { + elementStylingApply(0, ctx); + } + }, + template: (rf: RenderFlags, cmp: StaticHostClass) => {} + }); + } + + /** */ + const App = createComponent('app', (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + element(0, 'static-host-class'); + } + }, 1, 0, [StaticHostClass]); + + const fixture = new ComponentFixture(App); + const hostBindingEl = fixture.hostElement.querySelector('static-host-class') as HTMLElement; + expect(hostBindingEl.className).toEqual('mat-toolbar'); + }); + + }); });