diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index bbbaad4daf..a616f87284 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 2987, - "main-es2015": 462235, + "main-es2015": 456890, "polyfills-es2015": 52503 } } @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime-es2015": 3097, - "main-es2015": 438671, + "main-es2015": 425216, "polyfills-es2015": 52503 } } diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 63c5ec4713..a66545d810 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 18214, + "main-es2015": 16787, "polyfills-es2015": 36808 } } @@ -30,7 +30,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 139487, + "main-es2015": 137226, "polyfills-es2015": 37494 } } @@ -39,7 +39,7 @@ "master": { "uncompressed": { "runtime-es2015": 2289, - "main-es2015": 268796, + "main-es2015": 254857, "polyfills-es2015": 36808, "5-es2015": 751 } @@ -49,7 +49,7 @@ "master": { "uncompressed": { "runtime-es2015": 2289, - "main-es2015": 228770, + "main-es2015": 226519, "polyfills-es2015": 36808, "5-es2015": 779 } @@ -60,7 +60,7 @@ "uncompressed": { "bundle": "TODO(i): temporarily increase the payload size limit from 105779 - this is due to a closure issue related to ESM reexports that still needs to be investigated", "bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.", - "bundle": 176433 + "bundle": 175498 } } } diff --git a/modules/benchmarks/src/styling/ng2/styling.ts b/modules/benchmarks/src/styling/ng2/styling.ts index af43821de8..b669654ae7 100644 --- a/modules/benchmarks/src/styling/ng2/styling.ts +++ b/modules/benchmarks/src/styling/ng2/styling.ts @@ -18,7 +18,7 @@ import {BrowserModule} from '@angular/platform-browser'; - + diff --git a/modules/benchmarks/src/tree/render3_function/index.ts b/modules/benchmarks/src/tree/render3_function/index.ts index 4a9f9394c4..32be3be0b8 100644 --- a/modules/benchmarks/src/tree/render3_function/index.ts +++ b/modules/benchmarks/src/tree/render3_function/index.ts @@ -25,7 +25,7 @@ export class TreeFunction { type: TreeFunction, selectors: [['tree']], decls: 5, - vars: 1, + vars: 2, template: function(rf: ɵRenderFlags, ctx: TreeFunction) { // bit of a hack TreeTpl(rf, ctx.data); @@ -34,6 +34,7 @@ export class TreeFunction { }); } +const TreeFunctionCmpDef = TreeFunction.ɵcmp as{decls: number, vars: number}; export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { if (rf & ɵRenderFlags.Create) { ɵɵelementStart(0, 'tree'); @@ -54,7 +55,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { ɵɵcontainerRefreshStart(3); { if (ctx.left != null) { - let rf0 = ɵɵembeddedViewStart(0, 5, 1); + let rf0 = ɵɵembeddedViewStart(0, 5, 2); { TreeTpl(rf0, ctx.left); } ɵɵembeddedViewEnd(); } @@ -63,7 +64,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { ɵɵcontainerRefreshStart(4); { if (ctx.right != null) { - let rf0 = ɵɵembeddedViewStart(0, 5, 1); + let rf0 = ɵɵembeddedViewStart(0, TreeFunctionCmpDef.decls, TreeFunctionCmpDef.vars); { TreeTpl(rf0, ctx.right); } ɵɵembeddedViewEnd(); } diff --git a/packages/common/src/directives/ng_class.ts b/packages/common/src/directives/ng_class.ts index 63ddbe198e..382d871fc1 100644 --- a/packages/common/src/directives/ng_class.ts +++ b/packages/common/src/directives/ng_class.ts @@ -157,4 +157,11 @@ export class NgClass implements DoCheck { }); } } + + // TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs + // We need to export this to make angular/flex-layout happy + // https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9 + setClass(value: string) { this.klass = value; } + setNgClass(value: any) { this.ngClass = value; } + applyChanges() { this.ngDoCheck(); } } diff --git a/packages/common/src/directives/ng_style.ts b/packages/common/src/directives/ng_style.ts index 73dab1a154..52d4dc45d7 100644 --- a/packages/common/src/directives/ng_style.ts +++ b/packages/common/src/directives/ng_style.ts @@ -85,4 +85,10 @@ export class NgStyle implements DoCheck { changes.forEachAddedItem((record) => this._setStyle(record.key, record.currentValue)); changes.forEachChangedItem((record) => this._setStyle(record.key, record.currentValue)); } + + // TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs + // We need to export this to make angular/flex-layout happy + // https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9 + setNgStyle(value: any) { this.ngStyle = value; } + applyChanges() { this.ngDoCheck(); } } diff --git a/packages/common/src/private_export.ts b/packages/common/src/private_export.ts index 038833ee04..8e07f8a780 100644 --- a/packages/common/src/private_export.ts +++ b/packages/common/src/private_export.ts @@ -6,5 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +// TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs +// We need to export this to make angular/flex-layout happy +// https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9 +export {NgClass as ɵNgClassImpl, NgClass as ɵNgClassR2Impl} from './directives/ng_class'; +export {NgStyle as ɵNgStyleR2Impl} from './directives/ng_style'; + export {DomAdapter as ɵDomAdapter, getDOM as ɵgetDOM, setRootDomAdapter as ɵsetRootDomAdapter} from './dom_adapter'; export {BrowserPlatformLocation as ɵBrowserPlatformLocation} from './location/platform_location'; \ No newline at end of file diff --git a/packages/common/test/directives/ng_class_spec.ts b/packages/common/test/directives/ng_class_spec.ts index d661c87cad..d31771932d 100644 --- a/packages/common/test/directives/ng_class_spec.ts +++ b/packages/common/test/directives/ng_class_spec.ts @@ -196,7 +196,7 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing'; fixture = createTestComponent(`
`); expect(() => fixture !.detectChanges()) .toThrowError( - /NgClass can only toggle CSS classes expressed as strings, got: \[object Object\]/); + /NgClass can only toggle CSS classes expressed as strings, got \[object Object\]/); }); }); @@ -372,6 +372,27 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing'; detectChangesAndExpectClassName('color-red'); }); + it('should allow classes with trailing and leading spaces in [ngClass]', () => { + @Component({ + template: ` +
+
+ ` + }) + class Cmp { + applyClasses = true; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const leading = fixture.nativeElement.querySelector('[leading-space]'); + const trailing = fixture.nativeElement.querySelector('[trailing-space]'); + expect(leading.className).toBe('foo'); + expect(trailing.className).toBe('foo'); + }); + }); }); } @@ -379,8 +400,7 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing'; @Component({selector: 'test-cmp', template: ''}) class TestComponent { condition: boolean = true; - // TODO(issue/24571): remove '!'. - items !: any[]; + items: any[]|undefined; arrExpr: string[] = ['foo']; setExpr: Set = new Set(); objExpr: {[klass: string]: any}|null = {'foo': true, 'bar': false}; diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts index bfa2910236..5d84cef4e1 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts @@ -856,7 +856,7 @@ describe('compiler compliance: bindings', () => { type: HostAttributeDir, selectors: [["", "hostAttributeDir", ""]], hostAttrs: ["title", "hello there from directive", ${AttributeMarker.Classes}, "one", "two", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"], - hostVars: 2, + hostVars: 4, hostBindings: function HostAttributeDir_HostBindings(rf, ctx, elIndex) { … } diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index ee8b92c419..16f38ed057 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -1006,15 +1006,8 @@ describe('compiler compliance: styling', () => { const template = ` hostAttrs: [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"], - hostVars: 6, + hostVars: 8, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { -<<<<<<< HEAD -======= - if (rf & 1) { - $r3$.ɵɵallocHostVars(8); - $r3$.ɵɵelementHostAttrs($e0_attrs$); - } ->>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction if (rf & 2) { $r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵclassMap(ctx.myClass); @@ -1068,14 +1061,8 @@ describe('compiler compliance: styling', () => { }; const template = ` - hostVars: 8, + hostVars: 12, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { -<<<<<<< HEAD -======= - if (rf & 1) { - $r3$.ɵɵallocHostVars(12); - } ->>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction if (rf & 2) { $r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵclassMap(ctx.myClasses); @@ -1143,14 +1130,8 @@ describe('compiler compliance: styling', () => { `; const hostBindings = ` - hostVars: 6, + hostVars: 8, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { -<<<<<<< HEAD -======= - if (rf & 1) { - $r3$.ɵɵallocHostVars(8); - } ->>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction if (rf & 2) { $r3$.ɵɵstyleMap(ctx.myStyleExp, $r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵclassMap(ctx.myClassExp); @@ -1212,6 +1193,7 @@ describe('compiler compliance: styling', () => { // NOTE: IF YOU ARE CHANGING THIS COMPILER SPEC, YOU MAY NEED TO CHANGE THE DIRECTIVE // DEF THAT'S HARD-CODED IN `ng_class.ts`. const template = ` + … hostVars: 2, hostBindings: function ClassDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { @@ -1219,7 +1201,7 @@ describe('compiler compliance: styling', () => { } } … - hostVars: 2, + hostVars: 4, hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵstyleProp("width", ctx.myWidth); @@ -1227,7 +1209,7 @@ describe('compiler compliance: styling', () => { } } … - hostVars: 2, + hostVars: 4, hostBindings: function HeightDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵstyleProp("height", ctx.myHeight); @@ -1917,7 +1899,7 @@ describe('compiler compliance: styling', () => { }; const template = ` - hostVars: 4, + hostVars: 6, hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title); @@ -2079,14 +2061,8 @@ describe('compiler compliance: styling', () => { }; const template = ` - hostVars: 9, + hostVars: 10, hostBindings: function MyDir_HostBindings(rf, ctx, elIndex) { -<<<<<<< HEAD -======= - … - $r3$.ɵɵallocHostVars(10); - … ->>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction if (rf & 2) { $r3$.ɵɵhostProperty("title", ctx.title); $r3$.ɵɵupdateSyntheticHostBinding("@anim", diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 061888e97d..14c85acb07 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -2340,7 +2340,7 @@ runInEachFileSystem(os => { env.driveMain(); const jsContents = env.getContents('test.js'); const hostBindingsFn = ` - hostVars: 3, + hostVars: 4, hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody)("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); }); diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts index 96a701eb3a..e4b86bc076 100644 --- a/packages/compiler/src/render3/view/styling_builder.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -524,9 +524,10 @@ function isStyleSanitizable(prop: string): boolean { // Note that browsers support both the dash case and // camel case property names when setting through JS. return prop === 'background-image' || prop === 'backgroundImage' || prop === 'background' || - prop === 'border-image' || prop === 'borderImage' || prop === 'filter' || - prop === 'list-style' || prop === 'listStyle' || prop === 'list-style-image' || - prop === 'listStyleImage' || prop === 'clip-path' || prop === 'clipPath'; + prop === 'border-image' || prop === 'borderImage' || prop === 'border-image-source' || + prop === 'borderImageSource' || prop === 'filter' || prop === 'list-style' || + prop === 'listStyle' || prop === 'list-style-image' || prop === 'listStyleImage' || + prop === 'clip-path' || prop === 'clipPath'; } /** diff --git a/packages/compiler/test/render3/style_parser_spec.ts b/packages/compiler/test/render3/style_parser_spec.ts index 9fe365bca9..4f44f6aebb 100644 --- a/packages/compiler/test/render3/style_parser_spec.ts +++ b/packages/compiler/test/render3/style_parser_spec.ts @@ -21,6 +21,11 @@ describe('style parsing', () => { expect(result).toEqual(['width', '100px', 'height', '200px', 'opacity', '0']); }); + it('should allow empty values', () => { + const result = parseStyle('width:;height: ;'); + expect(result).toEqual(['width', '', 'height', '']); + }); + it('should trim values and properties', () => { const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;'); expect(result).toEqual(['width', '333px', 'height', '666px', 'opacity', '0.5']); diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts index e822917971..d639f88f2f 100644 --- a/packages/core/src/render3/assert.ts +++ b/packages/core/src/render3/assert.ts @@ -73,6 +73,11 @@ export function assertFirstCreatePass(tView: TView, errMessage?: string) { tView.firstCreatePass, true, errMessage || 'Should only be called in first create pass.'); } +export function assertFirstUpdatePass(tView: TView, errMessage?: string) { + assertEqual( + tView.firstUpdatePass, true, errMessage || 'Should only be called in first update pass.'); +} + /** * This is a basic sanity check that an object is probably a directive def. DirectiveDef is * an interface, so we can't do a direct instanceof check. diff --git a/packages/core/src/render3/bindings.ts b/packages/core/src/render3/bindings.ts index bb99394e49..e9e3edefc5 100644 --- a/packages/core/src/render3/bindings.ts +++ b/packages/core/src/render3/bindings.ts @@ -30,7 +30,19 @@ export function getBinding(lView: LView, bindingIndex: number): any { return lView[bindingIndex]; } -/** Updates binding if changed, then returns whether it was updated. */ +/** + * Updates binding if changed, then returns whether it was updated. + * + * This function also checks the `CheckNoChangesMode` and throws if changes are made. + * Some changes (Objects/iterables) during `CheckNoChangesMode` are exempt to comply with VE + * behavior. + * + * @param lView current `LView` + * @param bindingIndex The binding in the `LView` to check + * @param value New value to check against `lView[bindingIndex]` + * @returns `true` if the bindings has changed. (Throws if binding has changed during + * `CheckNoChangesMode`) + */ export function bindingUpdated(lView: LView, bindingIndex: number, value: any): boolean { ngDevMode && assertNotSame(value, NO_CHANGE, 'Incoming value should never be NO_CHANGE.'); ngDevMode && @@ -50,6 +62,11 @@ export function bindingUpdated(lView: LView, bindingIndex: number, value: any): throwErrorIfNoChangesMode( oldValue === NO_CHANGE, details.oldValue, details.newValue, details.propName); } + // There was a change, but the `devModeEqual` decided that the change is exempt from an error. + // For this reason we exit as if no change. The early exit is needed to prevent the changed + // value to be written into `LView` (If we would write the new value that we would not see it + // as change on next CD.) + return false; } lView[bindingIndex] = value; return true; diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 5b51fd0478..5ec1ec3c75 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -20,9 +20,10 @@ import {CLEAN_PROMISE, addHostBindingsToExpandoInstructions, addToViewTree, crea import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition'; import {TElementNode, TNode, TNodeType} from './interfaces/node'; import {PlayerHandler} from './interfaces/player'; -import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; +import {RElement, Renderer3, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer'; import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, RootContext, RootContextFlags, TVIEW, TViewType} from './interfaces/view'; -import {enterView, getPreviousOrParentTNode, incrementActiveDirectiveId, leaveView, setActiveHostElement} from './state'; +import {enterView, getPreviousOrParentTNode, leaveView, setActiveHostElement} from './state'; +import {writeDirectClass, writeDirectStyle} from './styling/reconcile'; import {computeStaticStyling} from './styling/static_styling'; import {setUpAttributes} from './util/attrs_utils'; import {publishDefaultGlobalUtils} from './util/global_utils'; @@ -119,8 +120,9 @@ export function renderComponent( // The first index of the first selector is the tag name. const componentTag = componentDef.selectors ![0] ![0] as string; + const hostRenderer = rendererFactory.createRenderer(null, null); const hostRNode = - locateHostElement(rendererFactory, opts.host || componentTag, componentDef.encapsulation); + locateHostElement(hostRenderer, opts.host || componentTag, componentDef.encapsulation); const rootFlags = componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot : LViewFlags.CheckAlways | LViewFlags.IsRoot; const rootContext = createRootContext(opts.scheduler, opts.playerHandler); @@ -137,7 +139,7 @@ export function renderComponent( try { if (rendererFactory.begin) rendererFactory.begin(); const componentView = createRootComponentView( - hostRNode, componentDef, rootView, rendererFactory, renderer, sanitizer); + hostRNode, componentDef, rootView, rendererFactory, renderer, null, sanitizer); component = createRootComponent( componentView, componentDef, rootView, rootContext, opts.hostFeatures || null); @@ -160,14 +162,15 @@ export function renderComponent( * @param rNode Render host element. * @param def ComponentDef * @param rootView The parent view where the host node is stored - * @param renderer The current renderer + * @param hostRenderer The current renderer * @param sanitizer The sanitizer, if provided * * @returns Component view created */ export function createRootComponentView( rNode: RElement | null, def: ComponentDef, rootView: LView, - rendererFactory: RendererFactory3, renderer: Renderer3, sanitizer?: Sanitizer | null): LView { + rendererFactory: RendererFactory3, hostRenderer: Renderer3, addVersion: string | null, + sanitizer: Sanitizer | null): LView { const tView = rootView[TVIEW]; ngDevMode && assertDataInRange(rootView, 0 + HEADER_OFFSET); rootView[0 + HEADER_OFFSET] = rNode; @@ -176,13 +179,27 @@ export function createRootComponentView( if (mergedAttrs !== null) { computeStaticStyling(tNode, mergedAttrs); if (rNode !== null) { - setUpAttributes(renderer, rNode, mergedAttrs); + setUpAttributes(hostRenderer, rNode, mergedAttrs); + if (tNode.classes !== null) { + writeDirectClass(hostRenderer, rNode, tNode.classes); + } + if (tNode.styles !== null) { + writeDirectStyle(hostRenderer, rNode, tNode.styles); + } } } + const viewRenderer = rendererFactory.createRenderer(rNode, def); + if (rNode !== null && addVersion) { + ngDevMode && ngDevMode.rendererSetAttribute++; + isProceduralRenderer(hostRenderer) ? + hostRenderer.setAttribute(rNode, 'ng-version', addVersion) : + rNode.setAttribute('ng-version', addVersion); + } + const componentView = createLView( rootView, getOrCreateTComponentView(def), null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[HEADER_OFFSET], tNode, - rendererFactory, renderer, sanitizer); + rendererFactory, viewRenderer, sanitizer); if (tView.firstCreatePass) { diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), tView, def.type); @@ -219,26 +236,17 @@ export function createRootComponent( } const rootTNode = getPreviousOrParentTNode(); - // TODO(misko-next): This is a temporary work around for the fact that we moved the information - // from instruction to declaration. The workaround is to just call the instruction as if it was - // part of the `hostAttrs`. - // The check for componentDef.hostBindings is wrong since now some directives may not - // have componentDef.hostBindings but they still need to process hostVars and hostAttrs if (tView.firstCreatePass && (componentDef.hostBindings !== null || componentDef.hostAttrs !== null)) { const elementIndex = rootTNode.index - HEADER_OFFSET; setActiveHostElement(elementIndex); - incrementActiveDirectiveId(); const rootTView = rootLView[TVIEW]; addHostBindingsToExpandoInstructions(rootTView, componentDef); growHostVarsSpace(rootTView, rootLView, componentDef.hostVars); invokeHostBindingsInCreationMode(componentDef, component, rootTNode); - - setActiveHostElement(null); } - return component; } diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 6bd001f66c..2b9b21c286 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -19,7 +19,6 @@ import {RendererFactory2} from '../render/api'; import {Sanitizer} from '../sanitization/sanitizer'; import {VERSION} from '../version'; import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider'; - import {assertComponentType} from './assert'; import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component'; import {getComponentDef} from './definition'; @@ -27,7 +26,7 @@ import {NodeInjector} from './di'; import {assignTViewNodeToLView, createLView, createTView, elementCreate, locateHostElement, renderView} from './instructions/shared'; import {ComponentDef} from './interfaces/definition'; import {TContainerNode, TElementContainerNode, TElementNode} from './interfaces/node'; -import {RNode, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer'; +import {RNode, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; import {LView, LViewFlags, TVIEW, TViewType} from './interfaces/view'; import {stringifyCSSSelectorList} from './node_selector_matcher'; import {enterView, leaveView} from './state'; @@ -132,8 +131,9 @@ export class ComponentFactory extends viewEngine_ComponentFactory { rootViewInjector.get(RendererFactory2, domRendererFactory3) as RendererFactory3; const sanitizer = rootViewInjector.get(Sanitizer, null); + const hostRenderer = rendererFactory.createRenderer(null, this.componentDef); const hostRNode = rootSelectorOrNode ? - locateHostElement(rendererFactory, rootSelectorOrNode, this.componentDef.encapsulation) : + locateHostElement(hostRenderer, rootSelectorOrNode, this.componentDef.encapsulation) : // Determine a tag name used for creating host elements when this component is created // dynamically. Default to 'div' if this component did not specify any tag name in its // selector. @@ -152,20 +152,13 @@ export class ComponentFactory extends viewEngine_ComponentFactory { /^#root-ng-internal-isolated-\d+/.test(rootSelectorOrNode); const rootContext = createRootContext(); - const renderer = rendererFactory.createRenderer(hostRNode, this.componentDef); - - if (rootSelectorOrNode && hostRNode) { - ngDevMode && ngDevMode.rendererSetAttribute++; - isProceduralRenderer(renderer) ? - renderer.setAttribute(hostRNode, 'ng-version', VERSION.full) : - hostRNode.setAttribute('ng-version', VERSION.full); - } // Create the root view. Uses empty TView and ContentTemplate. const rootTView = createTView(TViewType.Root, -1, null, 1, 0, null, null, null, null, null); const rootLView = createLView( - null, rootTView, rootContext, rootFlags, null, null, rendererFactory, renderer, sanitizer, - rootViewInjector); + null, rootTView, rootContext, rootFlags, null, null, rendererFactory, hostRenderer, + sanitizer, rootViewInjector); + const addVersion = rootSelectorOrNode && hostRNode ? VERSION.full : null; // rootView is the parent when bootstrapping // TODO(misko): it looks like we are entering view here but we don't really need to as @@ -179,7 +172,7 @@ export class ComponentFactory extends viewEngine_ComponentFactory { try { const componentView = createRootComponentView( - hostRNode, this.componentDef, rootLView, rendererFactory, renderer); + hostRNode, this.componentDef, rootLView, rendererFactory, hostRenderer, addVersion, null); tElementNode = getTNode(0, rootLView) as TElementNode; diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 57b8cb8058..72dd5d85c8 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -29,7 +29,6 @@ import {enterDI, leaveDI} from './state'; import {isNameOnlyAttributeMarker} from './util/attrs_utils'; import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './util/injector_utils'; import {stringifyForError} from './util/misc_utils'; -import {getInitialStylingValue} from './util/styling_utils'; @@ -269,10 +268,10 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer); ngDevMode && assertDefined(tNode, 'expecting tNode'); if (attrNameToInject === 'class') { - return getInitialStylingValue(tNode.classes); + return tNode.classes; } if (attrNameToInject === 'style') { - return getInitialStylingValue(tNode.styles); + return tNode.styles; } const attrs = tNode.attrs; diff --git a/packages/core/src/render3/instructions/advance.ts b/packages/core/src/render3/instructions/advance.ts index 307d9ece05..3ba2dcab3d 100644 --- a/packages/core/src/render3/instructions/advance.ts +++ b/packages/core/src/render3/instructions/advance.ts @@ -8,7 +8,7 @@ import {assertDataInRange, assertGreaterThan} from '../../util/assert'; import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks'; import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TVIEW} from '../interfaces/view'; -import {ActiveElementFlags, executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, hasActiveElementFlag, setSelectedIndex} from '../state'; +import {executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, setSelectedIndex} from '../state'; @@ -46,6 +46,7 @@ export function ɵɵadvance(delta: number): void { * @codeGenApi */ export function ɵɵselect(index: number): void { + // TODO(misko): Remove this function as it is no longer being used. selectIndexInternal(getLView(), index, getCheckNoChangesMode()); } @@ -53,9 +54,7 @@ export function selectIndexInternal(lView: LView, index: number, checkNoChangesM ngDevMode && assertGreaterThan(index, -1, 'Invalid index'); ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET); - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } + executeElementExitFn(); // Flush the initial hooks for elements in the view that have been added up to this point. // PERF WARNING: do NOT extract this to a separate function without running benchmarks diff --git a/packages/core/src/render3/instructions/class_map_interpolation.ts b/packages/core/src/render3/instructions/class_map_interpolation.ts index d1a59cf1b8..5387faf798 100644 --- a/packages/core/src/render3/instructions/class_map_interpolation.ts +++ b/packages/core/src/render3/instructions/class_map_interpolation.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {getLView, getSelectedIndex} from '../state'; - +import {getLView} from '../state'; +import {CLASS_MAP_STYLING_KEY} from '../styling/style_binding_list'; import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation'; -import {classMapInternal} from './styling'; +import {checkStylingMap} from './styling'; @@ -37,7 +37,7 @@ import {classMapInternal} from './styling'; export function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation1(lView, prefix, v0, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -67,7 +67,7 @@ export function ɵɵclassMapInterpolate2( prefix: string, v0: any, i0: string, v1: any, suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -100,7 +100,7 @@ export function ɵɵclassMapInterpolate3( prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -136,7 +136,7 @@ export function ɵɵclassMapInterpolate4( suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -175,7 +175,7 @@ export function ɵɵclassMapInterpolate5( const lView = getLView(); const interpolatedValue = interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -216,7 +216,7 @@ export function ɵɵclassMapInterpolate6( const lView = getLView(); const interpolatedValue = interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -259,7 +259,7 @@ export function ɵɵclassMapInterpolate7( const lView = getLView(); const interpolatedValue = interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -305,7 +305,7 @@ export function ɵɵclassMapInterpolate8( const lView = getLView(); const interpolatedValue = interpolation8( lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -334,5 +334,5 @@ export function ɵɵclassMapInterpolate8( export function ɵɵclassMapInterpolateV(values: any[]): void { const lView = getLView(); const interpolatedValue = interpolationV(lView, values); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 39ec60c5a7..c851ad43dd 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -10,20 +10,20 @@ import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert'; import {assertFirstCreatePass, assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; import {registerPostOrderHooks} from '../hooks'; -import {TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; +import {TAttributes, TElementNode, TNode, TNodeType, hasClassInput, hasStyleInput} from '../interfaces/node'; import {RElement} from '../interfaces/renderer'; -import {StylingMapArray, TStylingContext} from '../interfaces/styling'; import {isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks'; import {HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; -import {decreaseElementDepthCount, getBindingIndex, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state'; +import {decreaseElementDepthCount, getBindingIndex, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state'; +import {writeDirectClass, writeDirectStyle} from '../styling/reconcile'; +import {computeStaticStyling} from '../styling/static_styling'; import {setUpAttributes} from '../util/attrs_utils'; -import {getInitialStylingValue, hasClassInput, hasStyleInput, selectClassBasedInputName} from '../util/styling_utils'; -import {getConstant, getNativeByTNode, getTNode} from '../util/view_utils'; +import {getConstant} from '../util/view_utils'; +import {setDirectiveInputsWhichShadowsStyling} from './property'; +import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared'; -import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, matchingSchemas, renderInitialStyling, resolveDirectives, saveResolvedLocalsInData, setInputsForProperty} from './shared'; -import {registerInitialStylingOnTNode} from './styling'; function elementStartFirstCreatePass( index: number, tView: TView, lView: LView, native: RElement, name: string, @@ -40,7 +40,7 @@ function elementStartFirstCreatePass( ngDevMode && warnAboutUnknownElement(lView, native, tNode, hasDirectives); if (tNode.mergedAttrs !== null) { - registerInitialStylingOnTNode(tNode, tNode.mergedAttrs, 0); + computeStaticStyling(tNode, tNode.mergedAttrs); } if (tView.queries !== null) { @@ -88,8 +88,13 @@ export function ɵɵelementStart( if (mergedAttrs !== null) { setUpAttributes(renderer, native, mergedAttrs); } - if ((tNode.flags & TNodeFlags.hasInitialStyling) === TNodeFlags.hasInitialStyling) { - renderInitialStyling(renderer, native, tNode, false); + const classes = tNode.classes; + if (classes !== null) { + writeDirectClass(renderer, native, classes); + } + const styles = tNode.styles; + if (styles !== null) { + writeDirectStyle(renderer, native, styles); } appendChild(native, tNode, lView); @@ -143,17 +148,15 @@ export function ɵɵelementEnd(): void { } } - if (hasClassInput(tNode)) { - const inputName: string = selectClassBasedInputName(tNode.inputs !); - setDirectiveStylingInput(tNode.classes, lView, tNode.inputs ![inputName], inputName); + if (tNode.classes !== null && hasClassInput(tNode)) { + setDirectiveInputsWhichShadowsStyling(tNode, lView, tNode.classes, true); } - if (hasStyleInput(tNode)) { - setDirectiveStylingInput(tNode.styles, lView, tNode.inputs !['style'], 'style'); + if (tNode.styles !== null && hasStyleInput(tNode)) { + setDirectiveInputsWhichShadowsStyling(tNode, lView, tNode.styles, false); } } - /** * Creates an empty element using {@link elementStart} and {@link elementEnd} * @@ -170,20 +173,6 @@ export function ɵɵelement( ɵɵelementEnd(); } -function setDirectiveStylingInput( - context: TStylingContext | StylingMapArray | string | null, lView: LView, - stylingInputs: (string | number)[], propName: string) { - // older versions of Angular treat the input as `null` in the - // event that the value does not exist at all. For this reason - // we can't have a styling value be an empty string. - const value = (context && getInitialStylingValue(context)) || null; - - // Ivy does an extra `[class]` write with a falsy value since the value - // is applied during creation mode. This is a deviation from VE and should - // be (Jira Issue = FW-1467). - setInputsForProperty(lView, stylingInputs, propName, value); -} - function warnAboutUnknownElement( hostView: LView, element: RElement, tNode: TNode, hasDirectives: boolean): void { const schemas = hostView[TVIEW].schemas; diff --git a/packages/core/src/render3/instructions/element_container.ts b/packages/core/src/render3/instructions/element_container.ts index 8456ad1ff9..509a5888f9 100644 --- a/packages/core/src/render3/instructions/element_container.ts +++ b/packages/core/src/render3/instructions/element_container.ts @@ -15,10 +15,10 @@ import {HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from '../interface import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {getBindingIndex, getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state'; +import {computeStaticStyling} from '../styling/static_styling'; import {getConstant} from '../util/view_utils'; import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, resolveDirectives, saveResolvedLocalsInData} from './shared'; -import {registerInitialStylingOnTNode} from './styling'; function elementContainerStartFirstCreatePass( index: number, tView: TView, lView: LView, attrsIndex?: number | null, @@ -33,7 +33,7 @@ function elementContainerStartFirstCreatePass( // While ng-container doesn't necessarily support styling, we use the style context to identify // and execute directives on the ng-container. if (attrs !== null) { - registerInitialStylingOnTNode(tNode, attrs, 0); + computeStaticStyling(tNode, attrs); } const localRefs = getConstant(tViewConsts, localRefsIndex); diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index e6166ee6c3..791ef2bd43 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -14,15 +14,13 @@ import {initNgDevMode} from '../../util/ng_dev_mode'; import {ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container'; import {DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition'; import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from '../interfaces/i18n'; -import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node'; +import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node'; import {SelectorFlags} from '../interfaces/projection'; import {TQueries} from '../interfaces/query'; import {RComment, RElement, RNode} from '../interfaces/renderer'; -import {TStylingContext, TStylingRange} from '../interfaces/styling'; +import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate} from '../interfaces/styling'; import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, TViewType, T_HOST} from '../interfaces/view'; -import {DebugNodeStyling, NodeStylingDebug} from '../styling/styling_debug'; import {attachDebugObject} from '../util/debug_utils'; -import {isStylingContext} from '../util/styling_utils'; import {getLContainerActiveIndex, getTNode, unwrapRNode} from '../util/view_utils'; const NG_DEV_MODE = ((typeof ngDevMode === 'undefined' || !!ngDevMode) && initNgDevMode()); @@ -140,7 +138,7 @@ export const TViewConstructor = class TView implements ITView { public components: number[]|null, // public directiveRegistry: DirectiveDefList|null, // public pipeRegistry: PipeDefList|null, // - public firstChild: TNode|null, // + public firstChild: ITNode|null, // public schemas: SchemaMetadata[]|null, // public consts: TConstants|null, // ) {} @@ -152,7 +150,7 @@ export const TViewConstructor = class TView implements ITView { } }; -export const TNodeConstructor = class TNode implements ITNode { +class TNode implements ITNode { constructor( public tView_: TView, // public type: TNodeType, // @@ -176,8 +174,8 @@ export const TNodeConstructor = class TNode implements ITNode { public child: ITNode|null, // public parent: TElementNode|TContainerNode|null, // public projection: number|(ITNode|RNode[])[]|null, // - public styles: TStylingContext|null, // - public classes: TStylingContext|null, // + public styles: string|null, // + public classes: string|null, // public classBindings: TStylingRange, // public styleBindings: TStylingRange, // ) {} @@ -206,7 +204,6 @@ export const TNodeConstructor = class TNode implements ITNode { if (this.flags & TNodeFlags.hasClassInput) flags.push('TNodeFlags.hasClassInput'); if (this.flags & TNodeFlags.hasContentQuery) flags.push('TNodeFlags.hasContentQuery'); if (this.flags & TNodeFlags.hasStyleInput) flags.push('TNodeFlags.hasStyleInput'); - if (this.flags & TNodeFlags.hasInitialStyling) flags.push('TNodeFlags.hasInitialStyling'); if (this.flags & TNodeFlags.hasHostBindings) flags.push('TNodeFlags.hasHostBindings'); if (this.flags & TNodeFlags.isComponentHost) flags.push('TNodeFlags.isComponentHost'); if (this.flags & TNodeFlags.isDirectiveHost) flags.push('TNodeFlags.isDirectiveHost'); @@ -233,9 +230,54 @@ export const TNodeConstructor = class TNode implements ITNode { buf.push(''); return buf.join(''); } -}; -function processTNodeChildren(tNode: TNode | null, buf: string[]) { + get styleBindings_(): DebugStyleBindings { return toDebugStyleBinding(this, false); } + get classBindings_(): DebugStyleBindings { return toDebugStyleBinding(this, true); } +} +export const TNodeDebug = TNode; +export type TNodeDebug = TNode; + +export interface DebugStyleBindings extends Array { + [0]: string|null; +} +export interface DebugStyleBinding { + key: TStylingKey; + index: number; + isTemplate: boolean; + prevDuplicate: boolean; + nextDuplicate: boolean; + prevIndex: number; + nextIndex: number; +} + +function toDebugStyleBinding(tNode: TNode, isClassBased: boolean): DebugStyleBindings { + const tData = tNode.tView_.data; + const bindings: DebugStyleBindings = [] as any; + const range = isClassBased ? tNode.classBindings : tNode.styleBindings; + const prev = getTStylingRangePrev(range); + const next = getTStylingRangeNext(range); + let isTemplate = next !== 0; + let cursor = isTemplate ? next : prev; + while (cursor !== 0) { + const itemKey = tData[cursor] as TStylingKey; + const itemRange = tData[cursor + 1] as TStylingRange; + bindings.unshift({ + key: itemKey, + index: cursor, + isTemplate: isTemplate, + prevDuplicate: getTStylingRangePrevDuplicate(itemRange), + nextDuplicate: getTStylingRangeNextDuplicate(itemRange), + nextIndex: getTStylingRangeNext(itemRange), + prevIndex: getTStylingRangePrev(itemRange), + }); + if (cursor === prev) isTemplate = false; + cursor = getTStylingRangePrev(itemRange); + } + bindings.unshift(isClassBased ? tNode.classes : tNode.styles); + return bindings; +} + +function processTNodeChildren(tNode: ITNode | null, buf: string[]) { while (tNode) { buf.push((tNode as any as{template_: string}).template_); tNode = tNode.next; @@ -389,8 +431,6 @@ export class LViewDebug { export interface DebugNode { html: string|null; native: Node; - styles: DebugNodeStyling|null; - classes: DebugNodeStyling|null; nodes: DebugNode[]|null; component: LViewDebug|null; } @@ -401,10 +441,10 @@ export interface DebugNode { * @param tNode * @param lView */ -export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|null { +export function toDebugNodes(tNode: ITNode | null, lView: LView): DebugNode[]|null { if (tNode) { const debugNodes: DebugNode[] = []; - let tNodeCursor: TNode|null = tNode; + let tNodeCursor: ITNode|null = tNode; while (tNodeCursor) { debugNodes.push(buildDebugNode(tNodeCursor, lView, tNodeCursor.index)); tNodeCursor = tNodeCursor.next; @@ -415,19 +455,13 @@ export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|nul } } -export function buildDebugNode(tNode: TNode, lView: LView, nodeIndex: number): DebugNode { +export function buildDebugNode(tNode: ITNode, lView: LView, nodeIndex: number): DebugNode { const rawValue = lView[nodeIndex]; const native = unwrapRNode(rawValue); const componentLViewDebug = toDebug(readLViewValue(rawValue)); - const styles = isStylingContext(tNode.styles) ? - new NodeStylingDebug(tNode.styles as any as TStylingContext, tNode, lView, false) : - null; - const classes = isStylingContext(tNode.classes) ? - new NodeStylingDebug(tNode.classes as any as TStylingContext, tNode, lView, true) : - null; return { html: toHtml(native), - native: native as any, styles, classes, + native: native as any, nodes: toDebugNodes(tNode.child, lView), component: componentLViewDebug, }; diff --git a/packages/core/src/render3/instructions/property.ts b/packages/core/src/render3/instructions/property.ts index 524fb644fa..7abe5bf75c 100644 --- a/packages/core/src/render3/instructions/property.ts +++ b/packages/core/src/render3/instructions/property.ts @@ -6,11 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ import {bindingUpdated} from '../bindings'; +import {TNode} from '../interfaces/node'; import {SanitizerFn} from '../interfaces/sanitization'; -import {TVIEW} from '../interfaces/view'; +import {LView, TVIEW} from '../interfaces/view'; import {getLView, getSelectedIndex, nextBindingIndex} from '../state'; - -import {elementPropertyInternal, storePropertyBindingMetadata} from './shared'; +import {elementPropertyInternal, setInputsForProperty, storePropertyBindingMetadata} from './shared'; /** @@ -42,3 +42,16 @@ export function ɵɵproperty( } return ɵɵproperty; } + +/** + * Given `
` and `MyDir` with `@Input('style')` we need to write to + * directive input. + */ +export function setDirectiveInputsWhichShadowsStyling( + tNode: TNode, lView: LView, value: any, isClassBased: boolean) { + const inputs = tNode.inputs !; + const property = isClassBased ? 'class' : 'style'; + // We support both 'class' and `className` hence the fallback. + const stylingInputs = inputs[property] || (isClassBased && inputs['className']); + setInputsForProperty(lView, stylingInputs, property, value); +} diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index f8c87782f9..998b70bf7e 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -31,16 +31,14 @@ import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRoo import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, TViewType, T_HOST} from '../interfaces/view'; import {assertNodeOfPossibleTypes} from '../node_assert'; import {isNodeMatchingSelectorList} from '../node_selector_matcher'; -import {ActiveElementFlags, enterView, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, hasActiveElementFlag, incrementActiveDirectiveId, leaveView, leaveViewProcessExit, setActiveHostElement, setBindingIndex, setBindingRoot, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state'; -import {renderStylingMap, writeStylingValueDirectly} from '../styling/bindings'; +import {clearActiveHostElement, enterView, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, leaveView, leaveViewProcessExit, setActiveHostElement, setBindingIndex, setBindingRoot, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state'; import {NO_CHANGE} from '../tokens'; import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils'; import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils'; -import {getInitialStylingValue} from '../util/styling_utils'; import {getLViewParent} from '../util/view_traversal_utils'; import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils'; import {selectIndexInternal} from './advance'; -import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeConstructor, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug'; +import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug'; @@ -102,15 +100,6 @@ export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lVie } else { // If it's not a number, it's a host binding function that needs to be executed. if (instruction !== null) { - // Each directive gets a uniqueId value that is the same for both - // create and update calls when the hostBindings function is called. The - // directive uniqueId is not set anywhere--it is just incremented between - // each hostBindings call and is useful for helping instruction code - // uniquely determine which directive is currently active when executed. - // It is important that this be called first before the actual instructions - // are run because this way the first directive ID value is not zero. - incrementActiveDirectiveId(); - setBindingIndex(bindingRootIndex); const hostCtx = lView[currentDirectiveIndex]; instruction(RenderFlags.Update, hostCtx, currentElementIndex); @@ -123,10 +112,11 @@ export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lVie // iterate over those directives which actually have `hostBindings`. currentDirectiveIndex++; } + setBindingRoot(bindingRootIndex); } } } finally { - setActiveHostElement(selectedIndex); + clearActiveHostElement(); } } @@ -493,9 +483,13 @@ export function refreshView( incrementInitPhaseFlags(lView, InitPhaseState.AfterViewInitHooksToBeRun); } } - - } finally { if (tView.firstUpdatePass === true) { + // We need to make sure that we only flip the flag on successful `refreshView` only + // Don't do this in `finally` block. + // If we did this in `finally` block then an exception could block the execution of styling + // instructions which in turn would be unable to insert themselves into the styling linked + // list. The result of this would be that if the exception would not be throw on subsequent CD + // the styling would be unable to process it data and reflect to the DOM. tView.firstUpdatePass = false; } @@ -508,7 +502,7 @@ export function refreshView( if (!checkNoChangesMode) { lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass); } - + } finally { leaveViewProcessExit(); } } @@ -538,7 +532,7 @@ function executeTemplate( lView: LView, templateFn: ComponentTemplate, rf: RenderFlags, context: T) { const prevSelectedIndex = getSelectedIndex(); try { - setActiveHostElement(null); + clearActiveHostElement(); if (rf & RenderFlags.Update && lView.length > HEADER_OFFSET) { // When we're updating, inherently select 0 so we don't // have to generate that instruction for most update blocks. @@ -546,9 +540,7 @@ function executeTemplate( } templateFn(rf, context); } finally { - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } + executeElementExitFn(); setSelectedIndex(prevSelectedIndex); } } @@ -742,10 +734,8 @@ function assertHostNodeExists(rElement: RElement, elementOrSelector: RElement | * @param encapsulation View Encapsulation defined for component that requests host element. */ export function locateHostElement( - rendererFactory: RendererFactory3, elementOrSelector: RElement | string, + renderer: Renderer3, elementOrSelector: RElement | string, encapsulation: ViewEncapsulation): RElement { - const renderer = rendererFactory.createRenderer(null, null); - if (isProceduralRenderer(renderer)) { // When using native Shadow DOM, do not clear host element to allow native slot projection const preserveContent = encapsulation === ViewEncapsulation.ShadowDom; @@ -814,7 +804,7 @@ export function createTNode( adjustedIndex: number, tagName: string | null, attrs: TAttributes | null): TNode { ngDevMode && ngDevMode.tNode++; let injectorIndex = tParent ? tParent.injectorIndex : -1; - return ngDevMode ? new TNodeConstructor( + return ngDevMode ? new TNodeDebug( tView, // tView_: TView type, // type: TNodeType adjustedIndex, // index: number @@ -837,8 +827,8 @@ export function createTNode( null, // child: ITNode|null tParent, // parent: TElementNode|TContainerNode|null null, // projection: number|(ITNode|RNode[])[]|null - null, // styles: TStylingContext|null - null, // classes: TStylingContext|null + null, // styles: string|null + null, // classes: string|null 0 as any, // classBindings: TStylingRange; 0 as any, // styleBindings: TStylingRange; ) : @@ -1264,7 +1254,7 @@ function instantiateAllDirectives( } } -function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNode) { +function invokeDirectivesHostBindings(tView: TView, lView: LView, tNode: TNode) { const start = tNode.directiveStart; const end = tNode.directiveEnd; const expando = tView.expandoInstructions !; @@ -1272,25 +1262,27 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod const elementIndex = tNode.index - HEADER_OFFSET; try { setActiveHostElement(elementIndex); - for (let i = start; i < end; i++) { const def = tView.data[i] as DirectiveDef; - const directive = viewData[i]; + const directive = lView[i]; if (def.hostBindings !== null || def.hostVars !== 0 || def.hostAttrs !== null) { - // It is important that this be called first before the actual instructions - // are run because this way the first directive ID value is not zero. - incrementActiveDirectiveId(); invokeHostBindingsInCreationMode(def, directive, tNode); } else if (firstCreatePass) { expando.push(null); } } } finally { - setActiveHostElement(null); + clearActiveHostElement(); } } -// TODO(COMMIT): jsdoc +/** + * Invoke the host bindings in creation mode. + * + * @param def `DirectiveDef` which may contain the `hostBindings` function. + * @param directive Instance of directive. + * @param tNode Associated `TNode`. + */ export function invokeHostBindingsInCreationMode( def: DirectiveDef, directive: any, tNode: TNode) { if (def.hostBindings !== null) { @@ -1300,11 +1292,11 @@ export function invokeHostBindingsInCreationMode( } /** -* Generates a new block in TView.expandoInstructions for this node. -* -* Each expando block starts with the element index (turned negative so we can distinguish -* it from the hostVar count) and the directive count. See more in VIEW_DATA.md. -*/ + * Generates a new block in TView.expandoInstructions for this node. + * + * Each expando block starts with the element index (turned negative so we can distinguish + * it from the hostVar count) and the directive count. See more in VIEW_DATA.md. + */ export function generateExpandoInstructionBlock( tView: TView, tNode: TNode, directiveCount: number): void { ngDevMode && assertEqual( @@ -1993,4 +1985,3 @@ export function textBindingInternal(lView: LView, index: number, value: string): const renderer = lView[RENDERER]; isProceduralRenderer(renderer) ? renderer.setValue(element, value) : element.textContent = value; } - diff --git a/packages/core/src/render3/instructions/style_prop_interpolation.ts b/packages/core/src/render3/instructions/style_prop_interpolation.ts index 0bdadb76e1..a5d3ca7580 100644 --- a/packages/core/src/render3/instructions/style_prop_interpolation.ts +++ b/packages/core/src/render3/instructions/style_prop_interpolation.ts @@ -5,11 +5,10 @@ * 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 {getLView, getSelectedIndex} from '../state'; +import {getLView,} from '../state'; import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation'; -import {stylePropInternal} from './styling'; - +import {checkStylingProperty} from './styling'; /** @@ -43,7 +42,7 @@ export function ɵɵstylePropInterpolate1( valueSuffix?: string | null): typeof ɵɵstylePropInterpolate1 { const lView = getLView(); const interpolatedValue = interpolation1(lView, prefix, v0, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate1; } @@ -80,7 +79,7 @@ export function ɵɵstylePropInterpolate2( valueSuffix?: string | null): typeof ɵɵstylePropInterpolate2 { const lView = getLView(); const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate2; } @@ -119,7 +118,7 @@ export function ɵɵstylePropInterpolate3( valueSuffix?: string | null): typeof ɵɵstylePropInterpolate3 { const lView = getLView(); const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate3; } @@ -160,7 +159,7 @@ export function ɵɵstylePropInterpolate4( v3: any, suffix: string, valueSuffix?: string | null): typeof ɵɵstylePropInterpolate4 { const lView = getLView(); const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate4; } @@ -205,7 +204,7 @@ export function ɵɵstylePropInterpolate5( const lView = getLView(); const interpolatedValue = interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate5; } @@ -252,7 +251,7 @@ export function ɵɵstylePropInterpolate6( const lView = getLView(); const interpolatedValue = interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate6; } @@ -302,7 +301,7 @@ export function ɵɵstylePropInterpolate7( const lView = getLView(); const interpolatedValue = interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate7; } @@ -354,7 +353,7 @@ export function ɵɵstylePropInterpolate8( const lView = getLView(); const interpolatedValue = interpolation8( lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate8; } @@ -392,6 +391,6 @@ export function ɵɵstylePropInterpolateV( prop: string, values: any[], valueSuffix?: string | null): typeof ɵɵstylePropInterpolateV { const lView = getLView(); const interpolatedValue = interpolationV(lView, values); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolateV; } diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts index 5a592831fa..2418a15dbb 100644 --- a/packages/core/src/render3/instructions/styling.ts +++ b/packages/core/src/render3/instructions/styling.ts @@ -7,22 +7,22 @@ */ import {SafeValue} from '../../sanitization/bypass'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; -import {throwErrorIfNoChangesMode} from '../errors'; -import {setInputsForProperty} from '../instructions/shared'; -import {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; +import {assertEqual, assertGreaterThan, assertLessThan} from '../../util/assert'; +import {concatStringsWithSpace} from '../../util/stringify'; +import {assertFirstUpdatePass} from '../assert'; +import {bindingUpdated} from '../bindings'; +import {TNode, TNodeFlags, TNodeType} from '../interfaces/node'; import {RElement} from '../interfaces/renderer'; -import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from '../interfaces/styling'; -import {isDirectiveHost} from '../interfaces/type_checks'; -import {LView, RENDERER, TVIEW} from '../interfaces/view'; -import {getActiveDirectiveId, getCheckNoChangesMode, getCurrentStyleSanitizer, getLView, getSelectedIndex, incrementBindingIndex, nextBindingIndex, resetCurrentStyleSanitizer, setCurrentStyleSanitizer, setElementExitFn} from '../state'; -import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from '../styling/bindings'; -import {activateStylingMapFeature} from '../styling/map_based_bindings'; -import {attachStylingDebugObject} from '../styling/styling_debug'; +import {SanitizerFn} from '../interfaces/sanitization'; +import {TStylingKey, TStylingMapKey, TStylingSanitizationKey, TStylingSuffixKey, getTStylingRangeTail} from '../interfaces/styling'; +import {HEADER_OFFSET, RENDERER, TVIEW, TView} from '../interfaces/view'; +import {getCheckNoChangesMode, getClassBindingChanged, getCurrentStyleSanitizer, getLView, getSelectedIndex, getStyleBindingChanged, incrementBindingIndex, isActiveHostElement, markStylingBindingDirty, setCurrentStyleSanitizer, setElementExitFn} from '../state'; +import {writeAndReconcileClass, writeAndReconcileStyle} from '../styling/reconcile'; +import {CLASS_MAP_STYLING_KEY, IGNORE_DUE_TO_INPUT_SHADOW, STYLE_MAP_STYLING_KEY, flushStyleBinding, insertTStylingBinding} from '../styling/style_binding_list'; import {NO_CHANGE} from '../tokens'; -import {renderStringify} from '../util/misc_utils'; -import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, getValue, hasClassInput, hasStyleInput, hasValueChanged, hasValueChangedUnwrapSafeValue, isHostStylingActive, isStylingContext, isStylingMapArray, isStylingValueDefined, normalizeIntoStylingMap, patchConfig, selectClassBasedInputName, setValue, stylingMapToString} from '../util/styling_utils'; -import {getNativeByTNode, getTNode} from '../util/view_utils'; +import {unwrapRNode} from '../util/view_utils'; +import {setDirectiveInputsWhichShadowsStyling} from './property'; @@ -68,9 +68,9 @@ export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void { * @codeGenApi */ export function ɵɵstyleProp( - prop: string, value: string | number | SafeValue | null, + prop: string, value: string | number | SafeValue | null | undefined, suffix?: string | null): typeof ɵɵstyleProp { - stylePropInternal(getSelectedIndex(), prop, value, suffix); + checkStylingProperty(prop, value, suffix, false); return ɵɵstyleProp; } @@ -89,32 +89,9 @@ export function ɵɵstyleProp( * * @codeGenApi */ -export function ɵɵclassProp(className: string, value: boolean | null): typeof ɵɵclassProp { - // if a value is interpolated then it may render a `NO_CHANGE` value. - // in this case we do not need to do anything, but the binding index - // still needs to be incremented because all styling binding values - // are stored inside of the lView. - const bindingIndex = nextBindingIndex(); - const lView = getLView(); - const elementIndex = getSelectedIndex(); - const tNode = getTNode(elementIndex, lView); - const firstUpdatePass = lView[TVIEW].firstUpdatePass; - - // we check for this in the instruction code so that the context can be notified - // about prop or map bindings so that the direct apply check can decide earlier - // if it allows for context resolution to be bypassed. - if (firstUpdatePass) { - patchConfig(tNode, TNodeFlags.hasClassPropBindings); - patchHostStylingFlag(tNode, isHostStyling(), true); - } - - const updated = stylingProp(tNode, firstUpdatePass, lView, bindingIndex, className, value, true); - if (ngDevMode) { - ngDevMode.classProp++; - if (updated) { - ngDevMode.classPropCacheMiss++; - } - } +export function ɵɵclassProp( + className: string, value: boolean | null | undefined): typeof ɵɵclassProp { + checkStylingProperty(className, value, null, true); return ɵɵclassProp; } @@ -138,39 +115,10 @@ export function ɵɵclassProp(className: string, value: boolean | null): typeof * * @codeGenApi */ -export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void { - const index = getSelectedIndex(); - const lView = getLView(); - const tNode = getTNode(index, lView); - const firstUpdatePass = lView[TVIEW].firstUpdatePass; - const context = getStylesContext(tNode); - const hasDirectiveInput = hasStyleInput(tNode); - - // if a value is interpolated then it may render a `NO_CHANGE` value. - // in this case we do not need to do anything, but the binding index - // still needs to be incremented because all styling binding values - // are stored inside of the lView. - const bindingIndex = incrementBindingIndex(2); - const hostBindingsMode = isHostStyling(); - - // inputs are only evaluated from a template binding into a directive, therefore, - // there should not be a situation where a directive host bindings function - // evaluates the inputs (this should only happen in the template function) - if (!hostBindingsMode && hasDirectiveInput && styles !== NO_CHANGE) { - updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false, firstUpdatePass); - styles = NO_CHANGE; - } - - // we check for this in the instruction code so that the context can be notified - // about prop or map bindings so that the direct apply check can decide earlier - // if it allows for context resolution to be bypassed. - if (firstUpdatePass) { - patchConfig(tNode, TNodeFlags.hasStyleMapBindings); - patchHostStylingFlag(tNode, isHostStyling(), false); - } - - stylingMap( - context, tNode, firstUpdatePass, lView, bindingIndex, styles, false, hasDirectiveInput); +export function ɵɵstyleMap( + styles: {[styleName: string]: any} | Map| string | null | + undefined): void { + checkStylingMap(STYLE_MAP_STYLING_KEY, styles, false); } /** @@ -191,7 +139,224 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu * * @codeGenApi */ -export function ɵɵclassMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void { - classMapInternal(getSelectedIndex(), classes); +export function ɵɵclassMap( + classes: {[className: string]: boolean | null | undefined} | + Map| Set| string[] | string | null | undefined): void { + checkStylingMap(CLASS_MAP_STYLING_KEY, classes, true); } + +/** + * Common code between `ɵɵclassProp` and `ɵɵstyleProp`. + * + * @param prop property name. + * @param value binding value. + * @param suffixOrSanitizer suffix or sanitization function + * @param isClassBased `true` if `class` change (`false` if `style`) + */ +export function checkStylingProperty( + prop: string, value: any | NO_CHANGE, + suffixOrSanitizer: SanitizerFn | string | null | undefined, isClassBased: boolean): void { + const lView = getLView(); + const tView = lView[TVIEW]; + // Styling instructions use 2 slots per binding. + // 1. one for the value / TStylingKey + // 2. one for the intermittent-value / TStylingRange + const bindingIndex = incrementBindingIndex(2); + if (tView.firstUpdatePass) { + // This is a work around. Once PR#34480 lands the sanitizer is passed explicitly and this line + // can be removed. + let styleSanitizer: StyleSanitizeFn|null; + if (suffixOrSanitizer == null) { + if (styleSanitizer = getCurrentStyleSanitizer()) { + suffixOrSanitizer = styleSanitizer as any; + } + } + stylingPropertyFirstUpdatePass(tView, prop, suffixOrSanitizer, bindingIndex, isClassBased); + } + if (value !== NO_CHANGE && bindingUpdated(lView, bindingIndex, value)) { + markStylingBindingDirty(bindingIndex, isClassBased); + setElementExitFn(flushStylingOnElementExit); + } +} + +/** +* Common code between `ɵɵclassMap` and `ɵɵstyleMap`. +* +* @param tStylingMapKey See `STYLE_MAP_STYLING_KEY` and `CLASS_MAP_STYLING_KEY`. +* @param value binding value. +* @param isClassBased `true` if `class` change (`false` if `style`) +*/ +export function checkStylingMap( + tStylingMapKey: TStylingMapKey, value: any | NO_CHANGE, isClassBased: boolean): void { + const lView = getLView(); + const tView = lView[TVIEW]; + const bindingIndex = incrementBindingIndex(2); + if (tView.firstUpdatePass) { + stylingPropertyFirstUpdatePass(tView, tStylingMapKey, null, bindingIndex, isClassBased); + } + if (value !== NO_CHANGE && bindingUpdated(lView, bindingIndex, value)) { + // `getSelectedIndex()` should be here (rather than in instruction) so that it is guarded by the + // if so as not to read unnecessarily. + const tNode = tView.data[getSelectedIndex() + HEADER_OFFSET] as TNode; + if (hasStylingInputShadow(tNode, isClassBased) && !isInHostBindings(tView, bindingIndex)) { + // VE concatenates the static portion with the dynamic portion. + // We are doing the same. + let staticPrefix = isClassBased ? tNode.classes : tNode.styles; + ngDevMode && isClassBased === false && staticPrefix !== null && + assertEqual( + staticPrefix.endsWith(';'), true, 'Expecting static portion to end with \';\''); + if (typeof value === 'string') { + value = concatStringsWithSpace(staticPrefix, value as string); + } + // Given `
` such that `my-dir` has `@Input('style')`. + // This takes over the `[style]` binding. (Same for `[class]`) + setDirectiveInputsWhichShadowsStyling(tNode, lView, value, isClassBased); + } else { + markStylingBindingDirty(bindingIndex, isClassBased); + setElementExitFn(flushStylingOnElementExit); + } + } +} + +/** + * Determines when the binding is in `hostBindings` section + * + * @param tView Current `TView` + * @param bindingIndex index of binding which we would like if it is in `hostBindings` + */ +function isInHostBindings(tView: TView, bindingIndex: number): boolean { + // All host bindings are placed after the expando section. + return bindingIndex >= tView.expandoStartIndex; +} + +/** +* Collects the necessary information to insert the binding into a linked list of style bindings +* using `insertTStylingBinding`. +* +* @param tView `TView` where the binding linked list will be stored. +* @param prop Property/key of the binding. +* @param suffix Optional suffix or Sanitization function. +* @param bindingIndex Index of binding associated with the `prop` +* @param isClassBased `true` if `class` change (`false` if `style`) +*/ +function stylingPropertyFirstUpdatePass( + tView: TView, prop: TStylingMapKey, suffix: null, bindingIndex: number, + isClassBased: boolean): void; +function stylingPropertyFirstUpdatePass( + tView: TView, prop: string, suffix: SanitizerFn | string | null | undefined, + bindingIndex: number, isClassBased: boolean): void; +function stylingPropertyFirstUpdatePass( + tView: TView, prop: string | TStylingMapKey, + suffixOrSanitization: SanitizerFn | string | null | undefined, bindingIndex: number, + isClassBased: boolean): void { + ngDevMode && assertFirstUpdatePass(tView); + const tData = tView.data; + if (tData[bindingIndex + 1] === null) { + // The above check is necessary because we don't clear first update pass until first successful + // (no exception) template execution. This prevents the styling instruction from double adding + // itself to the list. + // `getSelectedIndex()` should be here (rather than in instruction) so that it is guarded by the + // if so as not to read unnecessarily. + const tNode = tData[getSelectedIndex() + HEADER_OFFSET] as TNode; + if (hasStylingInputShadow(tNode, isClassBased) && typeof prop === 'object' && + !isInHostBindings(tView, bindingIndex)) { + // typeof prop === 'object' implies that we are either `STYLE_MAP_STYLING_KEY` or + // `CLASS_MAP_STYLING_KEY` which means that we are either `[style]` or `[class]` binding. + // If there is a directive which uses `@Input('style')` or `@Input('class')` than + // we need to neutralize this binding since that directive is shadowing it. + // We turn this into a noop using `IGNORE_DUE_TO_INPUT_SHADOW` + prop = IGNORE_DUE_TO_INPUT_SHADOW; + } + const tStylingKey: TStylingKey = suffixOrSanitization == null ? prop : ({ + key: prop as string, extra: suffixOrSanitization + } as TStylingSuffixKey | TStylingSanitizationKey); + insertTStylingBinding( + tData, tNode, tStylingKey, bindingIndex, isActiveHostElement(), isClassBased); + } +} + +/** + * Tests if the `TNode` has input shadow. + * + * An input shadow is when a directive steals (shadows) the input by using `@Input('style')` or + * `@Input('class')` as input. + * + * @param tNode `TNode` which we would like to see if it has shadow. +* @param isClassBased `true` if `class` (`false` if `style`) + */ +export function hasStylingInputShadow(tNode: TNode, isClassBased: boolean) { + return (tNode.flags & (isClassBased ? TNodeFlags.hasClassInput : TNodeFlags.hasStyleInput)) !== 0; +} + +/** +* Flushes styling into DOM element from the bindings. +* +* The function starts at `LFrame.stylingBindingChanged` and computes new styling information from +* the bindings progressing towards the tail of the list. At the end the resulting style is written +* into the DOM Element. +* +* This function is invoked from: +* 1. Template `advance` instruction. +* 2. HostBinding instruction. +*/ +function flushStylingOnElementExit() { + ngDevMode && assertEqual( + getStyleBindingChanged() > 0 || getClassBindingChanged() > 0, true, + 'Only expected to be here if binding has changed.'); + ngDevMode && + assertEqual( + getCheckNoChangesMode(), false, 'Should never get here during check no changes mode'); + const lView = getLView(); + const tView = lView[TVIEW]; + const tData = tView.data; + const elementIndex = getSelectedIndex() + HEADER_OFFSET; + const tNode = tData[elementIndex] as TNode; + const renderer = lView[RENDERER]; + const element = unwrapRNode(lView[elementIndex]) as RElement; + + const classBindingIndex = getClassBindingChanged(); + if (classBindingIndex > 0) { + const classLastWrittenValueIndex = getTStylingRangeTail(tNode.classBindings) + 1; + ngDevMode && + assertGreaterThan( + classLastWrittenValueIndex, 1, + 'Ignoring `class` binding because there is no `class` metadata associated with the element. ' + + '(Was exception thrown during `firstUpdatePass` which prevented the metadata creation?)'); + ngDevMode && + assertLessThan(classLastWrittenValueIndex, lView.length, 'Reading past end of LView'); + const lastValue: string|NO_CHANGE = lView[classLastWrittenValueIndex]; + const newValue = flushStyleBinding(tData, tNode, lView, classBindingIndex, true); + if (lastValue !== newValue) { + if (tNode.type === TNodeType.Element) { + writeAndReconcileClass( + renderer, element, lastValue === NO_CHANGE ? tNode.classes || '' : lastValue as string, + newValue); + } + lView[classLastWrittenValueIndex] = newValue; + } + } + + const styleBindingIndex = getStyleBindingChanged(); + if (styleBindingIndex > 0) { + const styleLastWrittenValueIndex = getTStylingRangeTail(tNode.styleBindings) + 1; + ngDevMode && + assertGreaterThan( + styleLastWrittenValueIndex, 1, + 'Ignoring `style` binding because there is no `style` metadata associated with the element. ' + + '(Was exception thrown during `firstUpdatePass` which prevented the metadata creation?)'); + ngDevMode && + assertLessThan(styleLastWrittenValueIndex, lView.length, 'Reading past end of LView'); + const lastValue: string|NO_CHANGE = lView[styleLastWrittenValueIndex]; + const newValue = flushStyleBinding(tData, tNode, lView, styleBindingIndex, false); + if (lastValue !== newValue) { + if (tNode.type === TNodeType.Element) { + writeAndReconcileStyle( + renderer, element, lastValue === NO_CHANGE ? tNode.styles || '' : lastValue as string, + newValue); + } + lView[styleLastWrittenValueIndex] = newValue; + } + } + ngDevMode && ngDevMode.flushStyling++; +} \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 5b0a20cfda..3992291252 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -77,111 +77,6 @@ export const enum TNodeFlags { * that actually have directives with host bindings. */ hasHostBindings = 0x80, - - /** Bit #9 - This bit is set if the node has initial styling */ - hasInitialStyling = 0x100, - - /** - * Bit #10 - Whether or not there are class-based map bindings present. - * - * Examples include: - * 1. `
` - * 2. `@HostBinding('class') x` - */ - hasClassMapBindings = 0x200, - - /** - * Bit #11 - Whether or not there are any class-based prop bindings present. - * - * Examples include: - * 1. `
` - * 2. `@HostBinding('class.name') x` - */ - hasClassPropBindings = 0x400, - - /** - * Bit #12 - whether or not there are any active [class] and [class.name] bindings - */ - hasClassPropAndMapBindings = hasClassMapBindings | hasClassPropBindings, - - /** - * Bit #13 - Whether or not the context contains one or more class-based template bindings. - * - * Examples include: - * 1. `
` - * 2. `
` - */ - hasTemplateClassBindings = 0x800, - - /** - * Bit #14 - Whether or not the context contains one or more class-based host bindings. - * - * Examples include: - * 1. `@HostBinding('class') x` - * 2. `@HostBinding('class.name') x` - */ - hasHostClassBindings = 0x1000, - - /** - * Bit #15 - Whether or not there are two or more sources for a class property in the context. - * - * Examples include: - * 1. prop + prop: `
` - * 2. map + prop: `
` - * 3. map + map: `
` - */ - hasDuplicateClassBindings = 0x2000, - - /** - * Bit #16 - Whether or not there are style-based map bindings present. - * - * Examples include: - * 1. `
` - * 2. `@HostBinding('style') x` - */ - hasStyleMapBindings = 0x4000, - - /** - * Bit #17 - Whether or not there are any style-based prop bindings present. - * - * Examples include: - * 1. `
` - * 2. `@HostBinding('style.prop') x` - */ - hasStylePropBindings = 0x8000, - - /** - * Bit #18 - whether or not there are any active [style] and [style.prop] bindings - */ - hasStylePropAndMapBindings = hasStyleMapBindings | hasStylePropBindings, - - /** - * Bit #19 - Whether or not the context contains one or more style-based template bindings. - * - * Examples include: - * 1. `
` - * 2. `
` - */ - hasTemplateStyleBindings = 0x10000, - - /** - * Bit #20 - Whether or not the context contains one or more style-based host bindings. - * - * Examples include: - * 1. `@HostBinding('style') x` - * 2. `@HostBinding('style.prop') x` - */ - hasHostStyleBindings = 0x20000, - - /** - * Bit #21 - Whether or not there are two or more sources for a style property in the context. - * - * Examples include: - * 1. prop + prop: `
` - * 2. map + prop: `
` - * 3. map + map: `
` - */ - hasDuplicateStyleBindings = 0x40000, } /** @@ -588,20 +483,8 @@ export interface TNode { * This field will be populated if and when: * * - There are one or more initial styles on an element (e.g. `
`) - * - There are one or more style bindings on an element (e.g. `
`) - * - * If and when there are only initial styles (no bindings) then an instance of `StylingMapArray` - * will be used here. Otherwise an instance of `TStylingContext` will be created when there - * are one or more style bindings on an element. - * - * During element creation this value is likely to be populated with an instance of - * `StylingMapArray` and only when the bindings are evaluated (which happens during - * update mode) then it will be converted to a `TStylingContext` if any style bindings - * are encountered. If and when this happens then the existing `StylingMapArray` value - * will be placed into the initial styling slot in the newly created `TStylingContext`. */ - // TODO(misko): `Remove StylingMapArray|TStylingContext|null` in follow up PR. - styles: StylingMapArray|TStylingContext|string|null; + styles: string|null; /** * A collection of all class bindings and/or static class values for an element. @@ -609,20 +492,8 @@ export interface TNode { * This field will be populated if and when: * * - There are one or more initial classes on an element (e.g. `
`) - * - There are one or more class bindings on an element (e.g. `
`) - * - * If and when there are only initial classes (no bindings) then an instance of `StylingMapArray` - * will be used here. Otherwise an instance of `TStylingContext` will be created when there - * are one or more class bindings on an element. - * - * During element creation this value is likely to be populated with an instance of - * `StylingMapArray` and only when the bindings are evaluated (which happens during - * update mode) then it will be converted to a `TStylingContext` if any class bindings - * are encountered. If and when this happens then the existing `StylingMapArray` value - * will be placed into the initial styling slot in the newly created `TStylingContext`. */ - // TODO(misko): `Remove StylingMapArray|TStylingContext|null` in follow up PR. - classes: StylingMapArray|TStylingContext|string|null; + classes: string|null; /** * Stores the head/tail index of the class bindings. @@ -842,3 +713,53 @@ export type TNodeWithLocalRefs = TContainerNode | TElementNode | TElementContain * - `` - `tplRef` should point to the `TemplateRef` instance; */ export type LocalRefExtractor = (tNode: TNodeWithLocalRefs, currentView: LView) => any; + +/** + * Returns `true` if the `TNode` has a directive which has `@Input()` for `class` binding. + * + * ``` + *
+ * ``` + * and + * ``` + * @Directive({ + * }) + * class MyDirective { + * @Input() + * class: string; + * } + * ``` + * + * In the above case it is necessary to write the reconciled styling information into the + * directive's input. + * + * @param tNode + */ +export function hasClassInput(tNode: TNode) { + return (tNode.flags & TNodeFlags.hasClassInput) !== 0; +} + +/** + * Returns `true` if the `TNode` has a directive which has `@Input()` for `style` binding. + * + * ``` + *
+ * ``` + * and + * ``` + * @Directive({ + * }) + * class MyDirective { + * @Input() + * class: string; + * } + * ``` + * + * In the above case it is necessary to write the reconciled styling information into the + * directive's input. + * + * @param tNode + */ +export function hasStyleInput(tNode: TNode) { + return (tNode.flags & TNodeFlags.hasStyleInput) !== 0; +} \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index cba639d879..fffdb5a8e2 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -642,3 +642,8 @@ export function getTStylingRangeNextDuplicate(tStylingRange: TStylingRange): boo export function setTStylingRangeNextDuplicate(tStylingRange: TStylingRange): TStylingRange { return ((tStylingRange as any as number) | StylingRange.NEXT_DUPLICATE) as any; } + +export function getTStylingRangeTail(tStylingRange: TStylingRange): number { + const next = getTStylingRangeNext(tStylingRange); + return next === 0 ? getTStylingRangePrev(tStylingRange) : next; +} \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index fb4e3ffb5e..9a7e4ee0f8 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -471,7 +471,17 @@ export interface TView { /** Whether or not this template has been processed in creation mode. */ firstCreatePass: boolean; - /** Whether or not the first update for this template has been processed. */ + /** + * Whether or not this template has been processed in update mode (e.g. change detected) + * + * `firstUpdatePass` is used by styling to set up `TData` to contain metadata about the styling + * instructions. (Mainly to build up a linked list of styling priority order.) + * + * Typically this function gets cleared after first execution. If exception is thrown then this + * flag can remain turned un until there is first successful (no exception) pass. This means that + * individual styling instructions keep track of if they have already been added to the linked + * list to prevent double adding. + */ firstUpdatePass: boolean; /** Static data equivalent of LView.data[]. Contains TNodes, PipeDefInternal or TI18n. */ diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index cad0ce8c71..4aaa4f751d 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -7,8 +7,7 @@ */ import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; -import {assertDefined, assertEqual} from '../util/assert'; - +import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert'; import {assertLViewOrUndefined} from './assert'; import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {TNode} from './interfaces/node'; @@ -44,7 +43,7 @@ interface LFrame { /** * Used to set the parent property when nodes are created and track query results. * - * This is used in conjection with `isParent`. + * This is used in conjunction with `isParent`. */ previousOrParentTNode: TNode; @@ -94,11 +93,6 @@ interface LFrame { currentSanitizer: StyleSanitizeFn|null; - /** - * Used when processing host bindings. - */ - currentDirectiveDef: DirectiveDef|ComponentDef|null; - /** * The root index from which pure function instructions should calculate their binding * indices. In component views, this is TView.bindingStartIndex. In a host binding @@ -111,6 +105,18 @@ interface LFrame { * We iterate over the list of Queries and increment current query index at every step. */ currentQueryIndex: number; + + /** + * Stores the index of the style binding which changed first. + * + * A change in styling binding implies that all bindings starting with this index need to be + * recomputed. See: `flushStylingOnElementExit` and `markStylingBindingDirty` functions for + * details. + * + * If this value is set then `flushStylingOnElementExit` needs to execute during the `advance` + * instruction to update the styling. + */ + stylingBindingChanged: number; } /** @@ -259,17 +265,15 @@ export function getLView(): LView { * function or when all host bindings are processed for an element). */ export const enum ActiveElementFlags { - Initial = 0b00, - RunExitFn = 0b01, + HostMode = 0b1, + RunExitFn = 0b1, Size = 1, } -/** - * Sets a flag is for the active element. - */ -function setActiveElementFlag(flag: ActiveElementFlags) { - instructionState.lFrame.selectedIndex |= flag; +export function isActiveHostElement(): boolean { + return (instructionState.lFrame.selectedIndex & ActiveElementFlags.HostMode) === + ActiveElementFlags.HostMode; } /** @@ -279,17 +283,23 @@ function setActiveElementFlag(flag: ActiveElementFlags) { * @param elementIndex the element index value for the host element where * the directive/component instance lives */ -export function setActiveHostElement(elementIndex: number | null) { - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } - setSelectedIndex(elementIndex === null ? -1 : elementIndex); - instructionState.lFrame.activeDirectiveId = 0; +export function setActiveHostElement(elementIndex: number) { + executeElementExitFn(); + setSelectedIndex(elementIndex); + instructionState.lFrame.selectedIndex |= ActiveElementFlags.HostMode; +} + +export function clearActiveHostElement() { + executeElementExitFn(); + setSelectedIndex(-1); } export function executeElementExitFn() { - instructionState.elementExitFn !(); - instructionState.lFrame.selectedIndex &= ~ActiveElementFlags.RunExitFn; + const lFrame = instructionState.lFrame; + if (lFrame.stylingBindingChanged !== 0) { + instructionState.elementExitFn !(); + lFrame.stylingBindingChanged = 0; + } } /** @@ -306,7 +316,6 @@ export function executeElementExitFn() { * @param fn */ export function setElementExitFn(fn: () => void): void { - setActiveElementFlag(ActiveElementFlags.RunExitFn); if (instructionState.elementExitFn === null) { instructionState.elementExitFn = fn; } @@ -354,6 +363,7 @@ export function getContextLView(): LView { } export function getCheckNoChangesMode(): boolean { + // TODO(misko): remove this from the LView since it is ngDevMode=true mode only. return instructionState.checkNoChangesMode; } @@ -430,8 +440,6 @@ export function enterDI(newView: LView, tNode: TNode) { newLFrame.elementDepthCount = DEV_MODE_VALUE; newLFrame.currentNamespace = DEV_MODE_VALUE; newLFrame.currentSanitizer = DEV_MODE_VALUE; - newLFrame.currentDirectiveDef = DEV_MODE_VALUE; - newLFrame.activeDirectiveId = DEV_MODE_VALUE; newLFrame.bindingRootIndex = DEV_MODE_VALUE; newLFrame.currentQueryIndex = DEV_MODE_VALUE; } @@ -471,11 +479,10 @@ export function enterView(newView: LView, tNode: TNode | null): void { newLFrame.elementDepthCount = 0; newLFrame.currentNamespace = null; newLFrame.currentSanitizer = null; - newLFrame.currentDirectiveDef = null; - newLFrame.activeDirectiveId = 0; newLFrame.bindingRootIndex = -1; newLFrame.bindingIndex = newView === null ? -1 : newView[TVIEW].bindingStartIndex; newLFrame.currentQueryIndex = 0; + newLFrame.stylingBindingChanged = 0; } /** @@ -498,22 +505,19 @@ function createLFrame(parent: LFrame | null): LFrame { elementDepthCount: 0, // currentNamespace: null, // currentSanitizer: null, // - currentDirectiveDef: null, // - activeDirectiveId: 0, // bindingRootIndex: -1, // bindingIndex: -1, // currentQueryIndex: 0, // parent: parent !, // child: null, // + stylingBindingChanged: 0, // }; parent !== null && (parent.child = lFrame); // link the new LFrame for reuse. return lFrame; } export function leaveViewProcessExit() { - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } + executeElementExitFn(); leaveView(); } @@ -539,7 +543,7 @@ function walkUpViews(nestingLevel: number, currentView: LView): LView { } /** - * Gets the most recent index passed to {@link select} + * Gets the currently selected element index. * * Used with {@link property} instruction (and more in the future) to identify the index in the * current `LView` to act on. @@ -616,3 +620,48 @@ export function getCurrentStyleSanitizer() { const lFrame = instructionState.lFrame; return lFrame === null ? null : lFrame.currentSanitizer; } + +/** + * Used for encoding both Class and Style index into `LFrame.stylingBindingChanged`. + */ +const enum BindingChanged { + CLASS_SHIFT = 16, + STYLE_MASK = 0xFFFF, +} + +/** + * Store the first binding location from where the style flushing should start. + * + * This function stores the first binding location. Any subsequent binding changes are ignored as + * they are downstream from this change and will be picked up once the flushing starts traversing + * forward. + * + * Because flushing for template and flushing for host elements are separate, we don't need to worry + * about the fact that they will be out of order. + * + * @param bindingIndex Index of binding location. This will be a binding location from which the + * flushing of styling should start. + * @param isClassBased `true` if `class` change (`false` if `style`) + */ +export function markStylingBindingDirty(bindingIndex: number, isClassBased: boolean) { + ngDevMode && assertGreaterThan(bindingIndex, 0, 'expected valid binding index changed'); + ngDevMode && + assertEqual( + getCheckNoChangesMode(), false, 'Should never get here during check no changes mode'); + const lFrame = instructionState.lFrame; + const stylingBindingChanged = lFrame.stylingBindingChanged; + const stylingBindingChangedExtracted = isClassBased ? + stylingBindingChanged >> BindingChanged.CLASS_SHIFT : + stylingBindingChanged & BindingChanged.STYLE_MASK; + if (stylingBindingChangedExtracted === 0) { + lFrame.stylingBindingChanged = stylingBindingChanged | + (isClassBased ? bindingIndex << BindingChanged.CLASS_SHIFT : bindingIndex); + } +} + +export function getClassBindingChanged() { + return instructionState.lFrame.stylingBindingChanged >> BindingChanged.CLASS_SHIFT; +} +export function getStyleBindingChanged() { + return instructionState.lFrame.stylingBindingChanged & BindingChanged.STYLE_MASK; +} \ No newline at end of file diff --git a/packages/core/src/render3/styling/class_differ.ts b/packages/core/src/render3/styling/class_differ.ts index 09375e08b1..24338ecc82 100644 --- a/packages/core/src/render3/styling/class_differ.ts +++ b/packages/core/src/render3/styling/class_differ.ts @@ -6,11 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +import {assertNotEqual} from '../../util/assert'; import {CharCode} from '../../util/char_code'; - +import {concatStringsWithSpace} from '../../util/stringify'; import {consumeWhitespace, getLastParsedKey, parseClassName, parseClassNameNext} from './styling_parser'; + /** * Computes the diff between two class-list strings. * @@ -92,17 +94,6 @@ export function processClassToken( } } -/** - * Removes a class from a `className` string. - * - * @param className A string containing classes (whitespace separated) - * @param classToRemove A class name to remove from the `className` - * @returns a new class-list which does not have `classToRemove` - */ -export function removeClass(className: string, classToRemove: string): string { - return toggleClass(className, classToRemove, false); -} - /** * Toggles a class in `className` string. * @@ -121,17 +112,23 @@ export function toggleClass(className: string, classToToggle: string, toggle: bo start = classIndexOf(className, classToToggle, start); if (start === -1) { if (toggle === true) { - className = className === '' ? classToToggle : className + ' ' + classToToggle; + className = concatStringsWithSpace(className, classToToggle); } break; } - const removeLength = classToToggle.length; if (toggle === true) { // we found it and we should have it so just return return className; } else { + const length = classToToggle.length; // Cut out the class which should be removed. - const endWhitespace = consumeWhitespace(className, start + removeLength, end); + const endWhitespace = consumeWhitespace(className, start + length, end); + if (endWhitespace === end) { + // If we are the last token then we need back search trailing whitespace. + while (start > 0 && className.charCodeAt(start - 1) <= CharCode.SPACE) { + start--; + } + } className = className.substring(0, start) + className.substring(endWhitespace, end); end = className.length; } @@ -151,15 +148,16 @@ export function toggleClass(className: string, classToToggle: string, toggle: bo */ export function classIndexOf( className: string, classToSearch: string, startingIndex: number): number { + ngDevMode && assertNotEqual(classToSearch, '', 'can not look for "" string.'); let end = className.length; while (true) { const foundIndex = className.indexOf(classToSearch, startingIndex); if (foundIndex === -1) return foundIndex; if (foundIndex === 0 || className.charCodeAt(foundIndex - 1) <= CharCode.SPACE) { // Ensure that it has leading whitespace - const removeLength = classToSearch.length; - if (foundIndex + removeLength === end || - className.charCodeAt(foundIndex + removeLength) <= CharCode.SPACE) { + const length = classToSearch.length; + if (foundIndex + length === end || + className.charCodeAt(foundIndex + length) <= CharCode.SPACE) { // Ensure that it has trailing whitespace return foundIndex; } diff --git a/packages/core/src/render3/styling/reconcile.ts b/packages/core/src/render3/styling/reconcile.ts index cd8ef0057b..9d6c78bfd3 100644 --- a/packages/core/src/render3/styling/reconcile.ts +++ b/packages/core/src/render3/styling/reconcile.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {assertDefined, assertString} from '../../util/assert'; import {ProceduralRenderer3, RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer'; import {computeClassChanges} from './class_differ'; import {computeStyleChanges} from './style_differ'; @@ -43,14 +44,11 @@ import {computeStyleChanges} from './style_differ'; */ export function writeAndReconcileClass( renderer: Renderer3, element: RElement, expectedValue: string, newValue: string): void { + ngDevMode && assertDefined(element, 'Expecting DOM element'); + ngDevMode && assertString(expectedValue, '\'oldValue\' should be a string'); + ngDevMode && assertString(newValue, '\'newValue\' should be a string'); if (element.className === expectedValue) { - // This is the simple/fast case where no one has written into element without our knowledge. - if (isProceduralRenderer(renderer)) { - renderer.setAttribute(element, 'class', newValue); - } else { - element.className = newValue; - } - ngDevMode && ngDevMode.rendererSetClassName++; + writeDirectClass(renderer, element, newValue); } else { // The expected value is not the same as last value. Something changed the DOM element without // our knowledge so we need to do reconciliation instead. @@ -58,6 +56,32 @@ export function writeAndReconcileClass( } } +/** + * Write `className` to `RElement`. + * + * This function does direct write without any reconciliation. Used for writing initial values, so + * that static styling values do not pull in the style parser. + * + * @param renderer Renderer to use + * @param element The element which needs to be updated. + * @param newValue The new class list to write. + */ +export function writeDirectClass(renderer: Renderer3, element: RElement, newValue: string) { + ngDevMode && assertString(newValue, '\'newValue\' should be a string'); + if (isProceduralRenderer(renderer)) { + if (newValue === '') { + // There are tests in `google3` which expect `element.getAttribute('class')` to be `null`. + // TODO(commit): add test case + renderer.removeAttribute(element, 'class'); + } else { + renderer.setAttribute(element, 'class', newValue); + } + } else { + element.className = newValue; + } + ngDevMode && ngDevMode.rendererSetClassName++; +} + /** * Writes new `cssText` value in the DOM node. * @@ -88,15 +112,12 @@ export function writeAndReconcileClass( */ export function writeAndReconcileStyle( renderer: Renderer3, element: RElement, expectedValue: string, newValue: string): void { - const style = (element as HTMLElement).style; - if (style != null && style.cssText === expectedValue) { - // This is the simple/fast case where no one has written into element without our knowledge. - if (isProceduralRenderer(renderer)) { - renderer.setAttribute(element, 'style', newValue); - } else { - style.cssText = newValue; - } - ngDevMode && ngDevMode.rendererCssText++; + ngDevMode && assertDefined(element, 'Expecting DOM element'); + ngDevMode && assertString(expectedValue, '\'expectedValue\' should be a string'); + ngDevMode && assertString(newValue, '\'newValue\' should be a string'); + const style = expectedValue === null ? null : (element as HTMLElement).style; + if (expectedValue === null || style != null && (style !.cssText === expectedValue)) { + writeDirectStyle(renderer, element, newValue); } else { // The expected value is not the same as last value. Something changed the DOM element without // our knowledge so we need to do reconciliation instead. @@ -104,6 +125,26 @@ export function writeAndReconcileStyle( } } +/** + * Write `cssText` to `RElement`. + * + * This function does direct write without any reconciliation. Used for writing initial values, so + * that static styling values do not pull in the style parser. + * + * @param renderer Renderer to use + * @param element The element which needs to be updated. + * @param newValue The new class list to write. + */ +export function writeDirectStyle(renderer: Renderer3, element: RElement, newValue: string) { + ngDevMode && assertString(newValue, '\'newValue\' should be a string'); + if (isProceduralRenderer(renderer)) { + renderer.setAttribute(element, 'style', newValue); + } else { + (element as HTMLElement).style.cssText = newValue; + } + ngDevMode && ngDevMode.rendererSetStyle++; +} + /** * Writes to `classNames` by computing the difference between `oldValue` and `newValue` and using * `classList.add` and `classList.remove`. diff --git a/packages/core/src/render3/styling/static_styling.ts b/packages/core/src/render3/styling/static_styling.ts index 845b16ae22..8944d8aae7 100644 --- a/packages/core/src/render3/styling/static_styling.ts +++ b/packages/core/src/render3/styling/static_styling.ts @@ -23,8 +23,8 @@ import {getLView} from '../state'; export function computeStaticStyling(tNode: TNode, attrs: TAttributes): void { ngDevMode && assertFirstCreatePass( getLView()[TVIEW], 'Expecting to be called in first template pass only'); - let styles: string|null = tNode.styles as string | null; - let classes: string|null = tNode.classes as string | null; + let styles: string|null = tNode.styles; + let classes: string|null = tNode.classes; let mode: AttributeMarker|0 = 0; for (let i = 0; i < attrs.length; i++) { const value = attrs[i]; diff --git a/packages/core/src/render3/styling/style_binding_list.ts b/packages/core/src/render3/styling/style_binding_list.ts index 1922a52e63..f73e28c33a 100644 --- a/packages/core/src/render3/styling/style_binding_list.ts +++ b/packages/core/src/render3/styling/style_binding_list.ts @@ -6,13 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ +import {unwrapSafeValue} from '../../sanitization/bypass'; import {stylePropNeedsSanitization, ɵɵsanitizeStyle} from '../../sanitization/sanitization'; -import {assertEqual, throwError} from '../../util/assert'; +import {assertEqual, assertString, throwError} from '../../util/assert'; +import {CharCode} from '../../util/char_code'; +import {concatStringsWithSpace} from '../../util/stringify'; +import {assertFirstUpdatePass} from '../assert'; import {TNode} from '../interfaces/node'; import {SanitizerFn} from '../interfaces/sanitization'; import {TStylingKey, TStylingMapKey, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, getTStylingRangePrevDuplicate, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling'; import {LView, TData, TVIEW} from '../interfaces/view'; import {getLView} from '../state'; +import {NO_CHANGE} from '../tokens'; import {splitClassList, toggleClass} from './class_differ'; import {StyleChangesMap, parseKeyValue, removeStyle} from './style_differ'; import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseStyleNext} from './styling_parser'; @@ -172,7 +177,7 @@ import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseS * NOTE: See `should support example in 'tnode_linked_list.ts' documentation` in * `tnode_linked_list_spec.ts` for working example. */ - +let __unused_const_as_closure_does_not_like_standalone_comment_blocks__: undefined; /** * Insert new `tStyleValue` at `TData` and link existing style bindings such that we maintain linked @@ -197,9 +202,7 @@ import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseS export function insertTStylingBinding( tData: TData, tNode: TNode, tStylingKey: TStylingKey, index: number, isHostBinding: boolean, isClassBinding: boolean): void { - ngDevMode && assertEqual( - getLView()[TVIEW].firstUpdatePass, true, - 'Should be called during \'firstUpdatePass` only.'); + ngDevMode && assertFirstUpdatePass(getLView()[TVIEW]); let tBindings = isClassBinding ? tNode.classBindings : tNode.styleBindings; let tmplHead = getTStylingRangePrev(tBindings); let tmplTail = getTStylingRangeNext(tBindings); @@ -255,7 +258,7 @@ export function insertTStylingBinding( // Now we need to update / compute the duplicates. // Starting with our location search towards head (least priority) markDuplicates( - tData, tStylingKey, index, (isClassBinding ? tNode.classes : tNode.styles) as string, true, + tData, tStylingKey, index, (isClassBinding ? tNode.classes : tNode.styles) || '', true, isClassBinding); markDuplicates(tData, tStylingKey, index, '', false, isClassBinding); @@ -347,6 +350,7 @@ function markDuplicates( cursor = isPrevDir ? getTStylingRangePrev(tStyleRangeAtCursor) : getTStylingRangeNext(tStyleRangeAtCursor); } + // We also need to process the static values. if (staticValues !== '' && // If we have static values to search !foundDuplicate // If we have duplicate don't bother since we are already marked as // duplicate @@ -355,6 +359,8 @@ function markDuplicates( // if we are a Map (and we have statics) we must assume duplicate foundDuplicate = true; } else if (staticValues != null) { + // If we found non-map then we iterate over its keys to determine if any of them match ours + // If we find a match than we mark it as duplicate. for (let i = isClassBinding ? parseClassName(staticValues) : parseStyle(staticValues); // i >= 0; // i = isClassBinding ? parseClassNameNext(staticValues, i) : @@ -367,6 +373,7 @@ function markDuplicates( } } if (foundDuplicate) { + // if we found a duplicate, than mark ourselves. tData[index + 1] = isPrevDir ? setTStylingRangePrevDuplicate(tStylingAtIndex) : setTStylingRangeNextDuplicate(tStylingAtIndex); } @@ -392,18 +399,21 @@ export function flushStyleBinding( // value and look up the previous concatenation as a starting point going forward. const lastUnchangedValueIndex = getTStylingRangePrev(tStylingRangeAtIndex); let text = lastUnchangedValueIndex === 0 ? - ((isClassBinding ? tNode.classes : tNode.styles) as string) : - lView[lastUnchangedValueIndex + 1] as string; + (isClassBinding ? tNode.classes : tNode.styles) : + lView[lastUnchangedValueIndex + 1] as string | NO_CHANGE; + if (text === null || text === NO_CHANGE) text = ''; + ngDevMode && assertString(text, 'Last unchanged value should be a string'); let cursor = index; while (cursor !== 0) { const value = lView[cursor]; const key = tData[cursor] as TStylingKey; const stylingRange = tData[cursor + 1] as TStylingRange; lView[cursor + 1] = text = appendStyling( - text, key, value, null, getTStylingRangePrevDuplicate(stylingRange), isClassBinding); + text as string, key, value, null, getTStylingRangePrevDuplicate(stylingRange), + isClassBinding); cursor = getTStylingRangeNext(stylingRange); } - return text; + return text as string; } @@ -452,17 +462,27 @@ export function appendStyling( if (hasPreviousDuplicate) { text = toggleClass(text, stylingKey as string, !!value); } else if (value) { - text = text === '' ? stylingKey as string : text + ' ' + stylingKey; + text = concatStringsWithSpace(text, stylingKey as string); } } else { + if (value === undefined) { + // If undefined than treat it as if we have no value. This means that we will fallback to the + // previous value (if any). + // `
` => `width: 10px`. + return text; + } if (hasPreviousDuplicate) { text = removeStyle(text, key); } - const keyValue = - key + ': ' + (typeof suffixOrSanitizer === 'function' ? - suffixOrSanitizer(value) : - (suffixOrSanitizer == null ? value : value + suffixOrSanitizer)); - text = text === '' ? keyValue : text + '; ' + keyValue; + if (value !== false && value !== null) { + // `
` => ``. (remove it) + // `
` => ``. (remove it) + value = typeof suffixOrSanitizer === 'function' ? suffixOrSanitizer(value) : + unwrapSafeValue(value); + const keyValue = key + ': ' + + (typeof suffixOrSanitizer === 'string' ? value + suffixOrSanitizer : value) + ';'; + text = concatStringsWithSpace(text, keyValue); + } } return text; } @@ -489,7 +509,10 @@ export const CLASS_MAP_STYLING_KEY: TStylingMapKey = { } else if (typeof value === 'object') { // We support maps for (let key in value) { - text = appendStyling(text, key, value[key], null, hasPreviousDuplicate, true); + if (key !== '') { + // We have to guard for `""` empty string as key since it will break search and replace. + text = appendStyling(text, key, value[key], null, hasPreviousDuplicate, true); + } } } else if (typeof value === 'string') { // We support strings @@ -500,7 +523,7 @@ export const CLASS_MAP_STYLING_KEY: TStylingMapKey = { changes.forEach((_, key) => text = appendStyling(text, key, true, null, true, true)); } else { // No duplicates, just append it. - text = text === '' ? value : text + ' ' + value; + text = concatStringsWithSpace(text, value); } } else { // All other cases are not supported. @@ -531,9 +554,12 @@ export const STYLE_MAP_STYLING_KEY: TStylingMapKey = { } else if (typeof value === 'object') { // We support maps for (let key in value) { - text = appendStyling( - text, key, value[key], stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null, - hasPreviousDuplicate, false); + if (key !== '') { + // We have to guard for `""` empty string as key since it will break search and replace. + text = appendStyling( + text, key, value[key], stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null, + hasPreviousDuplicate, false); + } } } else if (typeof value === 'string') { // We support strings @@ -548,7 +574,10 @@ export const STYLE_MAP_STYLING_KEY: TStylingMapKey = { true, false)); } else { // No duplicates, just append it. - text = text === '' ? value : text + '; ' + value; + if (value.charCodeAt(value.length - 1) !== CharCode.SEMI_COLON) { + value += ';'; + } + text = concatStringsWithSpace(text, value); } } else { // All other cases are not supported. @@ -557,3 +586,14 @@ export const STYLE_MAP_STYLING_KEY: TStylingMapKey = { return text; } }; + + +/** + * If we have `
` such that `my-dir` has `@Input('class')`, the `my-dir` captures + * the `[class]` binding, so that it no longer participates in the style bindings. For this case + * we use `IGNORE_DUE_TO_INPUT_SHADOW` so that `flushStyleBinding` ignores it. + */ +export const IGNORE_DUE_TO_INPUT_SHADOW: TStylingMapKey = { + key: null, + extra: (text: string, value: any, hasPreviousDuplicate: boolean): string => { return text;} +}; \ No newline at end of file diff --git a/packages/core/src/render3/styling/style_differ.ts b/packages/core/src/render3/styling/style_differ.ts index 80459b7ac4..1238d01ec4 100644 --- a/packages/core/src/render3/styling/style_differ.ts +++ b/packages/core/src/render3/styling/style_differ.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {getLastParsedKey, getLastParsedValue, getLastParsedValueEnd, parseStyle, parseStyleNext, resetParserState} from './styling_parser'; +import {concatStringsWithSpace} from '../../util/stringify'; +import {consumeWhitespace, getLastParsedKey, getLastParsedValue, parseStyle, parseStyleNext, resetParserState} from './styling_parser'; /** * Stores changes to Style values. @@ -111,18 +112,20 @@ export function removeStyle(cssText: string, styleToRemove: string): string { for (let i = parseStyle(cssText); i >= 0; i = parseStyleNext(cssText, i)) { const key = getLastParsedKey(cssText); if (key === styleToRemove) { + // Consume any remaining whitespace. + i = consumeWhitespace(cssText, i, cssText.length); if (lastValueEnd === 0) { cssText = cssText.substring(i); i = 0; } else if (i === cssText.length) { return cssText.substring(0, lastValueEnd); } else { - cssText = cssText.substring(0, lastValueEnd) + '; ' + cssText.substring(i); - i = lastValueEnd + 2; // 2 is for '; '.length(so that we skip the separator) + cssText = concatStringsWithSpace(cssText.substring(0, lastValueEnd), cssText.substring(i)); + i = lastValueEnd + 1; // 1 is for ';'.length(so that we skip the separator) } resetParserState(cssText); } - lastValueEnd = getLastParsedValueEnd(); + lastValueEnd = i; } return cssText; } diff --git a/packages/core/src/render3/styling/styling_parser.ts b/packages/core/src/render3/styling/styling_parser.ts index be097b9d8f..84f711049e 100644 --- a/packages/core/src/render3/styling/styling_parser.ts +++ b/packages/core/src/render3/styling/styling_parser.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {assertEqual, throwError} from '../../util/assert'; import {CharCode} from '../../util/char_code'; /** @@ -142,17 +143,16 @@ export function parseStyle(text: string): number { */ export function parseStyleNext(text: string, startIndex: number): number { const end = parserState.textEnd; - if (end === startIndex) { + let index = parserState.key = consumeWhitespace(text, startIndex, end); + if (end === index) { // we reached an end so just quit return -1; } - let index = parserState.keyEnd = consumeStyleKey(text, parserState.key = startIndex, end); - index = parserState.value = consumeSeparatorWithWhitespace(text, index, end, CharCode.COLON); + index = parserState.keyEnd = consumeStyleKey(text, index, end); + index = consumeSeparator(text, index, end, CharCode.COLON); + index = parserState.value = consumeWhitespace(text, index, end); index = parserState.valueEnd = consumeStyleValue(text, index, end); - if (ngDevMode && parserState.value === parserState.valueEnd) { - throw malformedStyleError(text, index); - } - return consumeSeparatorWithWhitespace(text, index, end, CharCode.SEMI_COLON); + return consumeSeparator(text, index, end, CharCode.SEMI_COLON); } /** @@ -167,15 +167,6 @@ export function resetParserState(text: string): void { parserState.textEnd = text.length; } -/** - * Retrieves tha `valueEnd` from the parser global state. - * - * See: `ParserState`. - */ -export function getLastParsedValueEnd(): number { - return parserState.valueEnd; -} - /** * Returns index of next non-whitespace character. * @@ -233,16 +224,15 @@ export function consumeStyleKey(text: string, startIndex: number, endIndex: numb * @param endIndex Ending index of character where the scan should end. * @returns Index after separator and surrounding whitespace. */ -export function consumeSeparatorWithWhitespace( +export function consumeSeparator( text: string, startIndex: number, endIndex: number, separator: number): number { startIndex = consumeWhitespace(text, startIndex, endIndex); if (startIndex < endIndex) { if (ngDevMode && text.charCodeAt(startIndex) !== separator) { - throw expectingError(text, String.fromCharCode(separator), startIndex); + malformedStyleError(text, String.fromCharCode(separator), startIndex); } startIndex++; } - startIndex = consumeWhitespace(text, startIndex, endIndex); return startIndex; } @@ -310,18 +300,14 @@ export function consumeQuotedText( ch1 = ch; } } - throw ngDevMode ? expectingError(text, String.fromCharCode(quoteCharCode), endIndex) : + throw ngDevMode ? malformedStyleError(text, String.fromCharCode(quoteCharCode), endIndex) : new Error(); } -function expectingError(text: string, expecting: string, index: number) { - return new Error( - `Expecting '${expecting}' at location ${index} in string '` + text.substring(0, index) + - '[>>' + text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.'); -} - -function malformedStyleError(text: string, index: number) { - return new Error( +function malformedStyleError(text: string, expecting: string, index: number): never { + ngDevMode && assertEqual(typeof text === 'string', true, 'String expected here'); + throw throwError( `Malformed style at location ${index} in string '` + text.substring(0, index) + '[>>' + - text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.'); + text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + + `'. Expecting '${expecting}'.`); } diff --git a/packages/core/src/render3/tokens.ts b/packages/core/src/render3/tokens.ts index 2ef57c0ab6..9c53282f2e 100644 --- a/packages/core/src/render3/tokens.ts +++ b/packages/core/src/render3/tokens.ts @@ -8,8 +8,9 @@ export interface NO_CHANGE { // This is a brand that ensures that this type can never match anything else - brand: 'NO_CHANGE'; + __brand__: 'NO_CHANGE'; } /** A special value which designates that a value has not changed. */ -export const NO_CHANGE = {} as NO_CHANGE; +export const NO_CHANGE: NO_CHANGE = + (typeof ngDevMode === 'undefined' || ngDevMode) ? {__brand__: 'NO_CHANGE'} : ({} as NO_CHANGE); diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts index 1108eee5f5..653f5a5b69 100644 --- a/packages/core/src/sanitization/sanitization.ts +++ b/packages/core/src/sanitization/sanitization.ts @@ -187,6 +187,12 @@ export function ɵɵsanitizeUrlOrResourceUrl(unsafeUrl: any, tag: string, prop: */ export const ɵɵdefaultStyleSanitizer = (function(prop: string, value: string|null, mode?: StyleSanitizeMode): string | boolean | null { + if (value === undefined && mode === undefined) { + // This is a workaround for the fact that `StyleSanitizeFn` should not exist once PR#34480 + // lands. For now the `StyleSanitizeFn` and should act like `(value: any) => string` as a + // work around. + return ɵɵsanitizeStyle(prop); + } mode = mode || StyleSanitizeMode.ValidateAndSanitize; let doSanitizeValue = true; if (mode & StyleSanitizeMode.ValidateProperty) { @@ -201,9 +207,11 @@ export const ɵɵdefaultStyleSanitizer = } as StyleSanitizeFn); export function stylePropNeedsSanitization(prop: string): boolean { - return prop === 'background-image' || prop === 'background' || prop === 'border-image' || - prop === 'filter' || prop === 'list-style' || prop === 'list-style-image' || - prop === 'clip-path'; + return prop === 'background-image' || prop === 'backgroundImage' || prop === 'background' || + prop === 'border-image' || prop === 'borderImage' || prop === 'border-image-source' || + prop === 'borderImageSource' || prop === 'filter' || prop === 'list-style' || + prop === 'listStyle' || prop === 'list-style-image' || prop === 'listStyleImage' || + prop === 'clip-path' || prop === 'clipPath'; } export function validateAgainstEventProperties(name: string) { diff --git a/packages/core/src/util/stringify.ts b/packages/core/src/util/stringify.ts index 0f789c8082..11c5f331f8 100644 --- a/packages/core/src/util/stringify.ts +++ b/packages/core/src/util/stringify.ts @@ -37,7 +37,6 @@ export function stringify(token: any): string { return newLineIndex === -1 ? res : res.substring(0, newLineIndex); } - /** * Concatenates two strings with separator, allocating new strings only when necessary. * @@ -50,4 +49,4 @@ export function concatStringsWithSpace(before: string | null, after: string | nu return (before == null || before === '') ? (after === null ? '' : after) : ((after == null || after === '') ? before : before + ' ' + after); -} \ No newline at end of file +} diff --git a/packages/core/test/acceptance/change_detection_spec.ts b/packages/core/test/acceptance/change_detection_spec.ts index 125ba447db..a5d6b41b2e 100644 --- a/packages/core/test/acceptance/change_detection_spec.ts +++ b/packages/core/test/acceptance/change_detection_spec.ts @@ -1536,16 +1536,15 @@ describe('change detection', () => { }); it('should include style prop name in case of style binding', () => { - const message = ivyEnabled ? - `Previous value for 'style.color': 'red'. Current value: 'green'` : - `Previous value: 'color: red'. Current value: 'color: green'`; + const message = ivyEnabled ? `Previous value for 'color': 'red'. Current value: 'green'` : + `Previous value: 'color: red'. Current value: 'color: green'`; expect(() => initWithTemplate('
')) .toThrowError(new RegExp(message)); }); it('should include class name in case of class binding', () => { const message = ivyEnabled ? - `Previous value for 'class.someClass': 'true'. Current value: 'false'` : + `Previous value for 'someClass': 'true'. Current value: 'false'` : `Previous value: 'someClass: true'. Current value: 'someClass: false'`; expect(() => initWithTemplate('
')) .toThrowError(new RegExp(message)); @@ -1574,16 +1573,15 @@ describe('change detection', () => { }); it('should include style prop name in case of host style bindings', () => { - const message = ivyEnabled ? - `Previous value for 'style.color': 'red'. Current value: 'green'` : - `Previous value: 'color: red'. Current value: 'color: green'`; + const message = ivyEnabled ? `Previous value for 'color': 'red'. Current value: 'green'` : + `Previous value: 'color: red'. Current value: 'color: green'`; expect(() => initWithHostBindings({'[style.color]': 'unstableColorExpression'})) .toThrowError(new RegExp(message)); }); it('should include class name in case of host class bindings', () => { const message = ivyEnabled ? - `Previous value for 'class.someClass': 'true'. Current value: 'false'` : + `Previous value for 'someClass': 'true'. Current value: 'false'` : `Previous value: 'someClass: true'. Current value: 'someClass: false'`; expect(() => initWithHostBindings({'[class.someClass]': 'unstableBooleanExpression'})) .toThrowError(new RegExp(message)); diff --git a/packages/core/test/acceptance/discover_utils_spec.ts b/packages/core/test/acceptance/discover_utils_spec.ts index 9aa1e1d7b5..e357331bb8 100644 --- a/packages/core/test/acceptance/discover_utils_spec.ts +++ b/packages/core/test/acceptance/discover_utils_spec.ts @@ -10,6 +10,8 @@ import {Component, Directive, HostBinding, InjectionToken, ViewChild} from '@ang import {isLView} from '@angular/core/src/render3/interfaces/type_checks'; import {CONTEXT} from '@angular/core/src/render3/interfaces/view'; import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {getElementStyles} from '@angular/core/testing/src/styling'; +import {expect} from '@angular/core/testing/src/testing_internal'; import {onlyInIvy} from '@angular/private/testing'; import {getHostElement, markDirty} from '../../src/render3/index'; @@ -473,11 +475,10 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils deprecated', () => const childDebug = getDebugNode(child) !; expect(childDebug.native).toBe(child); - expect(childDebug.styles).toBeTruthy(); - - const styles = childDebug.styles !.values; - expect(styles['width']).toEqual('200px'); - expect(styles['height']).toEqual('400px'); + expect(getElementStyles(child)).toEqual({ + width: '200px', + height: '400px', + }); }); }); }); diff --git a/packages/core/test/acceptance/host_binding_spec.ts b/packages/core/test/acceptance/host_binding_spec.ts index f6a018dadb..6de8c5c1e5 100644 --- a/packages/core/test/acceptance/host_binding_spec.ts +++ b/packages/core/test/acceptance/host_binding_spec.ts @@ -254,7 +254,7 @@ describe('host bindings', () => { } TestBed.configureTestingModule( - {declarations: [MyApp, ParentDir, ChildDir, SiblingDir]}); + {declarations: [MyApp, ParentDir, SiblingDir, ChildDir]}); const fixture = TestBed.createComponent(MyApp); const element = fixture.nativeElement; fixture.detectChanges(); @@ -262,10 +262,9 @@ describe('host bindings', () => { const childElement = element.querySelector('div'); // width/height values were set in all directives, but the sub-class directive - // (ChildDir) - // had priority over the parent directive (ParentDir) which is why its value won. It - // also - // won over Dir because the SiblingDir directive was evaluated later on. + // (ChildDir) had priority over the parent directive (ParentDir) which is why its + // value won. It also won over Dir because the SiblingDir directive was declared + // later in `declarations`. expect(childElement.style.width).toEqual('200px'); expect(childElement.style.height).toEqual('200px'); diff --git a/packages/core/test/acceptance/inherit_definition_feature_spec.ts b/packages/core/test/acceptance/inherit_definition_feature_spec.ts index 75492c979f..745df8146c 100644 --- a/packages/core/test/acceptance/inherit_definition_feature_spec.ts +++ b/packages/core/test/acceptance/inherit_definition_feature_spec.ts @@ -116,10 +116,10 @@ describe('inheritance', () => { 'Base.backgroundColor', 'Super.color', 'Sub2.width', // ]); if (ivyEnabled) { - expect(getDirectiveDef(BaseDirective) !.hostVars).toEqual(1); - expect(getDirectiveDef(SuperDirective) !.hostVars).toEqual(2); - expect(getDirectiveDef(Sub1Directive) !.hostVars).toEqual(3); - expect(getDirectiveDef(Sub2Directive) !.hostVars).toEqual(3); + expect(getDirectiveDef(BaseDirective) !.hostVars).toEqual(2); + expect(getDirectiveDef(SuperDirective) !.hostVars).toEqual(4); + expect(getDirectiveDef(Sub1Directive) !.hostVars).toEqual(6); + expect(getDirectiveDef(Sub2Directive) !.hostVars).toEqual(6); } }); }); diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index ca44ccf073..3f26cd3597 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -1032,7 +1032,7 @@ describe('acceptance integration tests', () => { fixture.componentInstance.value = false; fixture.detectChanges(); - expect(structuralCompEl.getAttribute('class')).toEqual(''); + expect(structuralCompEl.getAttribute('class')).toBeFalsy(); }); @Directive({selector: '[DirWithClass]'}) @@ -1071,7 +1071,7 @@ describe('acceptance integration tests', () => { it('should delegate initial styles to a [style] input binding if present on a directive on the same element', () => { - @Component({template: '
'}) + @Component({template: '
'}) class App { @ViewChild(DirWithStyleDirective) mockStyleDirective !: DirWithStyleDirective; @@ -1084,8 +1084,8 @@ describe('acceptance integration tests', () => { const styles = fixture.componentInstance.mockStyleDirective.stylesVal; // Use `toContain` since Ivy and ViewEngine have some slight differences in formatting. - expect(styles).toContain('width:100px'); - expect(styles).toContain('height:200px'); + expect(styles).toContain('width: 100px'); + expect(styles).toContain('height: 200px'); }); it('should update `[class]` and bindings in the provided directive if the input is matched', @@ -1122,7 +1122,7 @@ describe('acceptance integration tests', () => { fixture.detectChanges(); expect(fixture.componentInstance.mockStyleDirective.stylesVal) - .toEqual({'width': '200px', 'height': '500px'}); + .toEqual({width: '200px', height: '500px'}); }); onlyInIvy('Style binding merging works differently in Ivy') @@ -1177,8 +1177,8 @@ describe('acceptance integration tests', () => { } }) class DirWithSingleStylingBindings { - width: null|string = null; - height: null|string = null; + width: string|null|undefined = undefined; + height: string|null|undefined = undefined; activateXYZClass: boolean = false; } @@ -1214,8 +1214,8 @@ describe('acceptance integration tests', () => { expect(target.classList.contains('def')).toBeTruthy(); expect(target.classList.contains('xyz')).toBeTruthy(); - dirInstance.width = null; - dirInstance.height = null; + dirInstance.width = undefined; + dirInstance.height = undefined; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('100px'); @@ -1230,7 +1230,7 @@ describe('acceptance integration tests', () => { () => { @Directive({selector: '[Dir1WithStyle]', host: {'[style.width]': 'width'}}) class Dir1WithStyle { - width: null|string = null; + width: null|string|undefined = undefined; } @Directive({ @@ -1238,7 +1238,7 @@ describe('acceptance integration tests', () => { host: {'style': 'width: 111px', '[style.width]': 'width'} }) class Dir2WithStyle { - width: null|string = null; + width: null|string|undefined = undefined; } @Component( @@ -1246,10 +1246,10 @@ describe('acceptance integration tests', () => { class App { @ViewChild(Dir1WithStyle) dir1Instance !: Dir1WithStyle; @ViewChild(Dir2WithStyle) dir2Instance !: Dir2WithStyle; - width: string|null = null; + width: string|null|undefined = undefined; } - TestBed.configureTestingModule({declarations: [App, Dir1WithStyle, Dir2WithStyle]}); + TestBed.configureTestingModule({declarations: [App, Dir2WithStyle, Dir1WithStyle]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const {dir1Instance, dir2Instance} = fixture.componentInstance; @@ -1263,15 +1263,15 @@ describe('acceptance integration tests', () => { fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('999px'); - fixture.componentInstance.width = null; + fixture.componentInstance.width = undefined; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('222px'); - dir1Instance.width = null; + dir1Instance.width = undefined; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('333px'); - dir2Instance.width = null; + dir2Instance.width = undefined; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('111px'); @@ -1316,7 +1316,7 @@ describe('acceptance integration tests', () => { } TestBed.configureTestingModule( - {declarations: [App, Dir1WithStyling, Dir2WithStyling]}); + {declarations: [App, Dir2WithStyling, Dir1WithStyling]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const {dir1Instance, dir2Instance} = fixture.componentInstance; @@ -1325,7 +1325,7 @@ describe('acceptance integration tests', () => { expect(target.style.getPropertyValue('width')).toEqual('111px'); const compInstance = fixture.componentInstance; - compInstance.stylesExp = {width: '999px', height: null}; + compInstance.stylesExp = {width: '999px', height: undefined}; compInstance.classesExp = {one: true, two: false}; dir1Instance.stylesExp = {width: '222px'}; dir1Instance.classesExp = {two: true, three: false}; diff --git a/packages/core/test/acceptance/renderer_factory_spec.ts b/packages/core/test/acceptance/renderer_factory_spec.ts index 324d08b5a7..f10269cf63 100644 --- a/packages/core/test/acceptance/renderer_factory_spec.ts +++ b/packages/core/test/acceptance/renderer_factory_spec.ts @@ -73,7 +73,6 @@ describe('renderer factory lifecycle', () => { fixture.detectChanges(); expect(logs).toEqual( ['create', 'create', 'begin', 'some_component create', 'some_component update', 'end']); - logs = []; fixture.detectChanges(); expect(logs).toEqual(['begin', 'some_component update', 'end']); diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index 9a74b70d63..8fdd3da292 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -7,16 +7,472 @@ */ import {CommonModule} from '@angular/common'; import {Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, HostBinding, Input, NgModule, Renderer2, ViewChild, ViewContainerRef} from '@angular/core'; -import {getDebugNode} from '@angular/core/src/render3/util/discovery_utils'; +import {bypassSanitizationTrustStyle} from '@angular/core/src/sanitization/bypass'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed} from '@angular/core/testing'; +import {getElementClasses, getElementStyles, getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling'; import {By, DomSanitizer, SafeStyle} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; +import {ivyEnabled, modifiedInIvy, onlyInIvy} from '@angular/private/testing'; describe('styling', () => { beforeEach(ngDevModeResetPerfCounters); + describe('apply in prioritization order', () => { + it('should perform static bindings', () => { + @Component({template: `
`}) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + + const [staticDiv] = fixture.nativeElement.querySelectorAll('div'); + expect(getSortedClassName(staticDiv)).toEqual('STATIC'); + expect(getSortedStyle(staticDiv)).toEqual('color: blue;'); + }); + + it('should perform prop bindings', () => { + @Component({ + template: `
` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('dynamic'); + expect(getSortedStyle(div)).toEqual('color: blue; width: 100px;'); + }); + + onlyInIvy('style merging is ivy only feature').it('should perform map bindings', () => { + @Component({ + template: `
` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('dynamic'); + expect(getSortedStyle(div)).toEqual('color: blue; width: 100px;'); + }); + + onlyInIvy('style merging is ivy only feature') + .it('should perform interpolation bindings', () => { + @Component({ + // TODO(misko): change `style-x` to `style` once #34202 lands + template: `
` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('dynamic static'); + expect(getSortedStyle(div)).toEqual('color: blue;'); + }); + + onlyInIvy('style merging is ivy only feature').it('should support hostBindings', () => { + @Component({ + template: + `
` + }) + class Cmp { + } + @Directive({ + selector: '[my-host-bindings-1]', + host: {'class': 'HOST_STATIC_1', 'style': 'font-family: "c1"'} + }) + class Dir1 { + } + + @Directive({ + selector: '[my-host-bindings-2]', + host: {'class': 'HOST_STATIC_2', 'style': 'font-family: "c2"'} + }) + class Dir2 { + } + + TestBed.configureTestingModule({ + declarations: [ + // Order of directives in the template does not matter. + // Order of declarations matters as it determines the relative priority for overrides. + Dir1, + Dir2, + // Even thought component is at the end, it will still have lowest priority because + // components are special that way. + Cmp, + ] + }); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('HOST_STATIC_1 HOST_STATIC_2 STATIC'); + expect(getSortedStyle(div)).toEqual('color: blue; font-family: c2;'); + }); + + it('should support hostBindings inheritance', () => { + @Component({template: `
`}) + class Cmp { + } + @Directive({host: {'class': 'SUPER_STATIC', 'style': 'font-family: "super"; width: "1px";'}}) + class SuperDir { + } + @Directive({ + selector: '[my-host-bindings]', + host: {'class': 'HOST_STATIC', 'style': 'font-family: "host font"'} + }) + class Dir extends SuperDir { + } + + TestBed.configureTestingModule({declarations: [Cmp, Dir]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)) + .toEqual(ivyEnabled ? 'HOST_STATIC STATIC SUPER_STATIC' : 'HOST_STATIC STATIC'); + // Browsers keep the '"' around the font name, but Domino removes it some we do search and + // replace. Yes we could do `replace(/"/g, '')` but that fails on android. + expect(getSortedStyle(div).replace('"', '').replace('"', '')) + .toEqual( + ivyEnabled ? 'color: blue; font-family: host font; width: 1px;' : + 'color: blue; font-family: host font;'); + }); + + onlyInIvy('style merging is ivy only feature') + .it('should apply template classes in correct order', () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const classDiv = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(classDiv)).toEqual('STATIC bar foo'); + }); + + onlyInIvy('style merging is ivy only feature') + .it('should apply template styles in correct order', () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const styleDiv = fixture.nativeElement.querySelector('div'); + expect(getSortedStyle(styleDiv)) + .toEqual('background-color: yellow; color: blue; width: 110px;'); + }); + + it('should work with ngClass/ngStyle', () => { + @Component( + {template: `
`}) + class Cmp { + } + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('dynamic'); + expect(getSortedStyle(div)).toEqual('font-family: dynamic;'); + }); + }); + + describe('css variables', () => { + onlyInIvy('css variables').it('should support css variables', () => { + // This test only works in browsers which support CSS variables. + if (!(typeof getComputedStyle !== 'undefined' && typeof CSS !== 'undefined' && + typeof CSS.supports !== 'undefined' && CSS.supports('color', 'var(--fake-var)'))) + return; + @Component({ + template: ` +
+ CONTENT +
` + }) + class Cmp { + } + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + // document.body.appendChild(fixture.nativeElement); + fixture.detectChanges(); + + const span = fixture.nativeElement.querySelector('span') as HTMLElement; + expect(getComputedStyle(span).getPropertyValue('background-color')).toEqual('rgb(255, 0, 0)'); + }); + }); + + modifiedInIvy('shadow bindings include static portion') + .it('should bind [class] as input to directive', () => { + // VE Behavior https://stackblitz.com/edit/angular-cycpsf + // IVY behavior is slightly different see next test with same name. + @Component({ + template: ` +
+
+ ` + }) + class Cmp { + } + + @Directive({selector: '[dir-shadows-class-input]'}) + class DirectiveShadowsClassInput { + constructor(private elementRef: ElementRef) {} + @Input('class') + set klass(value: string) { + this.elementRef.nativeElement.setAttribute('shadow-class', value); + } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const [div1, div2] = fixture.nativeElement.querySelectorAll('div'); + // Static value `class="s1"` is always written to the DOM. + expect(div1.className).toEqual('s1'); + // VE passes the dynamic portion of `class` to the directive. + expect(div1.getAttribute('shadow-class')).toEqual('d1'); + // Interpolation `class="s2 {{'d2'}}"` does not have a static portion and so no value is + // written to DOM. + expect(div2.className).toEqual(''); + expect(div2.getAttribute('shadow-class')).toEqual('s2 d2'); + }); + + + onlyInIvy('shadow bindings include static portion') + .it('should bind [class] as input to directive', () => { + // VE Behavior https://stackblitz.com/edit/angular-cycpsf + // IVY behavior is slightly different see next test with same name. + @Component({ + template: ` +
+
+ ` + }) + class Cmp { + } + + @Directive({selector: '[dir-shadows-class-input]'}) + class DirectiveShadowsClassInput { + constructor(private elementRef: ElementRef) {} + @Input('class') + set klass(value: string) { + this.elementRef.nativeElement.setAttribute('shadow-class', value); + } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const [div1, div2] = fixture.nativeElement.querySelectorAll('div'); + // Static value `class="s1"` is always written to the DOM. + expect(div1.className).toEqual('s1'); + // VE has weird behavior where it calls the @Input('class') with either `class="static` or + // `[class]="dynamic"` but never both. This is determined at compile time. Due to locality + // we don't know if `[class]` is coming if we see `class` only. So we need to combine the + // static and dynamic parte. This results in slightly different calling sequence, but should + // result in the same final DOM. + expect(div1.getAttribute('shadow-class')).toEqual('s1 d1'); + + expect(div2.className).toEqual(''); + expect(div2.getAttribute('shadow-class')).toEqual('s2 d2'); + }); + + + modifiedInIvy('shadow bindings include static portion') + .it('should bind [style] as input to directive', () => { + // VE Behavior https://stackblitz.com/edit/angular-cycpsf + @Component({ + template: ` +
+ ` + }) + class Cmp { + } + + @Directive({selector: '[dir-shadows-style-input]'}) + class DirectiveShadowsClassInput { + constructor(private elementRef: ElementRef) {} + @Input('style') + set style(value: string) { + this.elementRef.nativeElement.setAttribute('shadow-style', value); + } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.style.cssText).toEqual('color: red;'); + // VE has weird behavior where it calls the @Input('class') with either `class="static` or + // `[class]="dynamic"` but never both. This is determined at compile time. Due to locality + // we + // don't know if `[class]` is coming if we see `class` only. So we need to combine the two + // This results in slightly different calling sequence, but should result in the same final + // DOM. + expect(div.getAttribute('shadow-style')).toEqual('width: 100px;'); + }); + + onlyInIvy('shadow bindings include static portion') + .it('should bind [style] as input to directive', () => { + // VE Behavior https://stackblitz.com/edit/angular-cycpsf + @Component({ + template: ` +
+ ` + }) + class Cmp { + } + + @Directive({selector: '[dir-shadows-style-input]'}) + class DirectiveShadowsClassInput { + constructor(private elementRef: ElementRef) {} + @Input('style') + set style(value: string) { + this.elementRef.nativeElement.setAttribute('shadow-style', value); + } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.style.cssText).toEqual('color: red;'); + // VE has weird behavior where it calls the @Input('class') with either `class="static` or + // `[class]="dynamic"` but never both. This is determined at compile time. Due to locality + // we + // don't know if `[class]` is coming if we see `class` only. So we need to combine the two + // This results in slightly different calling sequence, but should result in the same final + // DOM. + expect(div.getAttribute('shadow-style')).toEqual('color: red; width: 100px;'); + }); + + it('should prevent circular ExpressionChangedAfterItHasBeenCheckedError on shadow inputs', () => { + @Component({template: `
`}) + class Cmp { + } + + @Directive({selector: '[dir-shadows-class-input]'}) + class DirectiveShadowsClassInput { + @Input('class') + klass: string|undefined; + + @HostBinding('class') + get hostClasses() { return `${this.klass} SUFFIX`; } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + expect(() => fixture.detectChanges()).not.toThrow(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.className).toEqual('s1 SUFFIX'); + }); + + it('should recover from exceptions', () => { + @Component({ + template: ` +
+ +
+ ` + }) + class Cmp { + id = 'throw_id'; + klass: string|string[] = 'throw_klass'; + foo = `throw_foo`; + + maybeThrow(value: any) { + if (typeof value === 'string' && value.indexOf('throw') === 0) { + throw new Error(value); + } + return value; + } + } + + let myDirHostBinding = false; + @Directive({selector: '[my-dir]'}) + class MyDirective { + @HostBinding('class.myDir') + get myDir(): boolean { + if (myDirHostBinding === false) { + throw new Error('class.myDir'); + } + return myDirHostBinding; + } + } + + TestBed.configureTestingModule({declarations: [Cmp, MyDirective]}); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + const div = fixture.nativeElement.querySelector('div'); + const span = fixture.nativeElement.querySelector('span'); + + expect(() => fixture.detectChanges()).toThrowError(/throw_id/); + expect(div.id).toBeFalsy(); + expectClass(span).toEqual({}); + + cmp.id = 'myId'; + expect(() => fixture.detectChanges()).toThrowError(/throw_klass/); + expect(div.id).toEqual('myId'); + expectClass(span).toEqual({}); + + cmp.klass = ['BAR']; + expect(() => fixture.detectChanges()).toThrowError(/throw_foo/); + expect(div.id).toEqual('myId'); + expectClass(span).toEqual(ivyEnabled ? {BAR: true} : {}); + + cmp.foo = 'foo'; + expect(() => fixture.detectChanges()).toThrowError(/class.myDir/); + expect(div.id).toEqual('myId'); + expectClass(span).toEqual(ivyEnabled ? {BAR: true, foo: true} : {}); + + myDirHostBinding = true; + fixture.detectChanges(); + expect(div.id).toEqual('myId'); + expectClass(span).toEqual({BAR: true, foo: true, myDir: true}); + }); + it('should render inline style and class attribute values on the element before a directive is instantiated', () => { @Component({ @@ -197,16 +653,15 @@ describe('styling', () => { expect(div.style.backgroundImage).toBe('url("#test")'); onlyInIvy('perf counters').expectPerfCounters({ - styleProp: 2, - stylePropCacheMiss: 1, + rendererSetStyle: 1, tNode: 3, }); }); - it('should not throw if host style binding is on a template node', () => { - // This ex is a bit contrived. In real apps, you might have a shared class that is extended both - // by components with host elements and by directives on template nodes. In that case, the host - // styles for the template directives should just be ignored. + it('should not write to the native element if a directive shadows the class input', () => { + // This ex is a bit contrived. In real apps, you might have a shared class that is extended + // both by components with host elements and by directives on template nodes. In that case, the + // host styles for the template directives should just be ignored. @Directive({selector: 'ng-template[styleDir]', host: {'[style.display]': 'display'}}) class StyleDir { display = 'block'; @@ -217,10 +672,7 @@ describe('styling', () => { } TestBed.configureTestingModule({declarations: [MyApp, StyleDir]}); - expect(() => { - const fixture = TestBed.createComponent(MyApp); - fixture.detectChanges(); - }).not.toThrow(); + TestBed.createComponent(MyApp).detectChanges(); }); it('should be able to bind a SafeValue to clip-path', () => { @@ -496,23 +948,18 @@ describe('styling', () => { } } - // Ivy does an extra `[class]` write with a falsy value since the value - // is applied during creation mode. This is a deviation from VE and should - // be (Jira Issue = FW-1467). - let totalWrites = ivyEnabled ? 1 : 0; - TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]}); const fixture = TestBed.createComponent(Cmp); - expect(capturedClassBindingCount).toEqual(totalWrites++); + expect(capturedClassBindingCount).toEqual(0); fixture.detectChanges(); - expect(capturedClassBindingCount).toEqual(totalWrites++); + expect(capturedClassBindingCount).toEqual(1); expect(capturedClassBindingValue as any).toEqual('bar'); fixture.componentInstance.c = 'dynamic-bar'; fixture.detectChanges(); - expect(capturedClassBindingCount).toEqual(totalWrites++); + expect(capturedClassBindingCount).toEqual(2); expect(capturedClassBindingValue !).toEqual('dynamic-bar'); }); @@ -653,15 +1100,28 @@ describe('styling', () => { const fixture = TestBed.createComponent(Cmp); fixture.detectChanges(); - expect(capturedClassBindingCount).toEqual(1); - expect(capturedClassBindingValue !).toEqual('static-val'); + expect(capturedClassBindingCount) + .toEqual( + 2 + // '2' is not ideal as '1' would be preferred. + // The reason for two writes is that one is for the static + // `class="static-val"` and one for `[class]="c"`. This means that + // `class="static-val"` is written during the create block which is not ideal. + // To do this correctly we would have to delay the `class="static-val"` until + // the update block, but that would be expensive since it would require that we + // would check if we possibly have this situation on every `advance()` + // instruction. We don't think this is worth it, and we are just going to live + // with this. + ); + expect(capturedClassBindingValue).toEqual(null); expect(capturedMyClassBindingCount).toEqual(1); expect(capturedMyClassBindingValue !).toEqual('foo'); + capturedClassBindingCount = 0; fixture.componentInstance.c = 'dynamic-val'; fixture.detectChanges(); - expect(capturedClassBindingCount).toEqual(2); + expect(capturedClassBindingCount).toEqual(1); expect(capturedClassBindingValue !).toEqual('static-val dynamic-val'); expect(capturedMyClassBindingCount).toEqual(1); expect(capturedMyClassBindingValue !).toEqual('foo'); @@ -791,17 +1251,17 @@ describe('styling', () => { @Component({ template: `
+ [style.height]="h" + [style.opacity]="o" + style="width:200px; height:200px;" + [class.abc]="abc" + [class.xyz]="xyz">
` }) class Cmp { - w: string|null = '100px'; - h: string|null = '100px'; - o: string|null = '0.5'; + w: string|null|undefined = '100px'; + h: string|null|undefined = '100px'; + o: string|null|undefined = '0.5'; abc = true; xyz = false; } @@ -817,9 +1277,9 @@ describe('styling', () => { expect(element.classList.contains('abc')).toBeTruthy(); expect(element.classList.contains('xyz')).toBeFalsy(); - fixture.componentInstance.w = null; - fixture.componentInstance.h = null; - fixture.componentInstance.o = null; + fixture.componentInstance.w = undefined; + fixture.componentInstance.h = undefined; + fixture.componentInstance.o = undefined; fixture.componentInstance.abc = false; fixture.componentInstance.xyz = true; fixture.detectChanges(); @@ -829,6 +1289,14 @@ describe('styling', () => { expect(element.style.opacity).toBeFalsy(); expect(element.classList.contains('abc')).toBeFalsy(); expect(element.classList.contains('xyz')).toBeTruthy(); + + fixture.componentInstance.w = null; + fixture.componentInstance.h = null; + fixture.componentInstance.o = null; + fixture.detectChanges(); + expect(element.style.width).toBeFalsy(); + expect(element.style.height).toBeFalsy(); + expect(element.style.opacity).toBeFalsy(); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -846,14 +1314,14 @@ describe('styling', () => { @Component({ template: `
+ [dir-that-sets-width]="w1" + [another-dir-that-sets-width]="w2"> ` }) class Cmp { - w0: string|null = null; - w1: string|null = null; - w2: string|null = null; + w0: string|null|undefined = null; + w1: string|null|undefined = null; + w2: string|null|undefined = null; } TestBed.configureTestingModule( @@ -867,17 +1335,17 @@ describe('styling', () => { const element = fixture.nativeElement.querySelector('div'); expect(element.style.width).toEqual('100px'); - fixture.componentInstance.w0 = null; - fixture.detectChanges(); - - expect(element.style.width).toEqual('200px'); - - fixture.componentInstance.w1 = null; + fixture.componentInstance.w0 = undefined; fixture.detectChanges(); expect(element.style.width).toEqual('300px'); - fixture.componentInstance.w2 = null; + fixture.componentInstance.w2 = undefined; + fixture.detectChanges(); + + expect(element.style.width).toEqual('200px'); + + fixture.componentInstance.w1 = undefined; fixture.detectChanges(); expect(element.style.width).toBeFalsy(); @@ -920,7 +1388,8 @@ describe('styling', () => { opacity: string|null = '0.5'; @ViewChild(CompWithStyling, {static: true}) compWithStyling: CompWithStyling|null = null; - @ViewChild(DirWithStyling, {static: true}) dirWithStyling: DirWithStyling|null = null; + @ViewChild(DirWithStyling, {static: true}) + dirWithStyling: DirWithStyling|null = null; } TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]}); @@ -929,14 +1398,6 @@ describe('styling', () => { const component = fixture.componentInstance; const element = fixture.nativeElement.querySelector('comp-with-styling'); - const node = getDebugNode(element) !; - - const styles = node.styles !; - const config = styles.context.config; - expect(config.hasCollisions).toBeFalsy(); - expect(config.hasMapBindings).toBeFalsy(); - expect(config.hasPropBindings).toBeTruthy(); - expect(config.allowDirectStyling).toBeTruthy(); expect(element.style.opacity).toEqual('0.5'); expect(element.style.width).toEqual('900px'); @@ -958,8 +1419,8 @@ describe('styling', () => { expect(element.style.height).toEqual('100px'); expect(element.style.fontSize).toEqual('50px'); - // there is no need to flush styling since the styles are applied directly - expect(ngDevMode !.flushStyling).toEqual(0); + // once for the template flush and again for the host bindings + expect(ngDevMode !.flushStyling).toEqual(2); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -993,8 +1454,8 @@ describe('styling', () => { ` }) class Cmp { - opacity: string|null = '0.5'; - width: string|null = 'auto'; + opacity: string|null|undefined = '0.5'; + width: string|null|undefined = 'auto'; tplClass = true; } @@ -1004,40 +1465,36 @@ describe('styling', () => { const element = fixture.nativeElement.querySelector('comp-with-styling'); - const node = getDebugNode(element) !; - const styles = node.styles !; - const classes = node.classes !; - - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'color': 'red', - 'width': 'auto', - 'opacity': '0.5', + 'font-size': '100px', 'height': '900px', - 'font-size': '100px' + 'opacity': '0.5', + 'width': 'auto', }); - expect(classes.values).toEqual({ + expectClass(element).toEqual({ 'dir': true, 'comp': true, 'tpl': true, }); - fixture.componentInstance.width = null; - fixture.componentInstance.opacity = null; + fixture.componentInstance.width = undefined; + fixture.componentInstance.opacity = undefined; fixture.componentInstance.tplClass = false; fixture.detectChanges(); - expect(styles.values).toEqual({ - 'color': 'red', - 'width': '900px', - 'opacity': null, - 'height': '900px', - 'font-size': '100px' - }); - expect(classes.values).toEqual({ + expectStyle(element).toEqual( + {'color': 'red', 'width': '900px', 'height': '900px', 'font-size': '100px'}); + expectClass(element).toEqual({ 'dir': true, 'comp': true, - 'tpl': false, }); + + fixture.componentInstance.width = null; + fixture.componentInstance.opacity = null; + fixture.detectChanges(); + + expectStyle(element).toEqual({'color': 'red', 'height': '900px', 'font-size': '100px'}); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -1059,7 +1516,7 @@ describe('styling', () => { ` }) class Cmp { - w3: string|null = '300px'; + w3: string|null|undefined = '300px'; } TestBed.configureTestingModule( @@ -1069,80 +1526,23 @@ describe('styling', () => { const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; - - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '300px', }); fixture.componentInstance.w3 = null; fixture.detectChanges(); - expect(styles.values).toEqual({ + expectStyle(element).toEqual({}); + + fixture.componentInstance.w3 = undefined; + fixture.detectChanges(); + + expectStyle(element).toEqual({ 'width': '200px', }); }); - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should support situations where there are more than 32 bindings', () => { - const TOTAL_BINDINGS = 34; - - let bindingsHTML = ''; - let bindingsArr: any[] = []; - for (let i = 0; i < TOTAL_BINDINGS; i++) { - bindingsHTML += `[style.prop${i}]="bindings[${i}]" `; - bindingsArr.push(null); - } - - @Component({template: `
`}) - class Cmp { - bindings = bindingsArr; - - updateBindings(value: string) { - for (let i = 0; i < TOTAL_BINDINGS; i++) { - this.bindings[i] = value + i; - } - } - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - - let testValue = 'initial'; - fixture.componentInstance.updateBindings('initial'); - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - - const node = getDebugNode(element) !; - const styles = node.styles !; - - let values = styles.values; - let props = Object.keys(values); - expect(props.length).toEqual(TOTAL_BINDINGS); - - for (let i = 0; i < props.length; i++) { - const prop = props[i]; - const value = values[prop] as string; - const num = value.substr(testValue.length); - expect(value).toEqual(`initial${num}`); - } - - testValue = 'final'; - fixture.componentInstance.updateBindings('final'); - fixture.detectChanges(); - - values = styles.values; - props = Object.keys(values); - expect(props.length).toEqual(TOTAL_BINDINGS); - for (let i = 0; i < props.length; i++) { - const prop = props[i]; - const value = values[prop] as string; - const num = value.substr(testValue.length); - expect(value).toEqual(`final${num}`); - } - }); onlyInIvy('only ivy has style debugging support') .it('should apply map-based style and class entries', () => { @@ -1176,23 +1576,8 @@ describe('styling', () => { fixture.detectChanges(); const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - let styles = node.styles !; - let classes = node.classes !; - - let stylesSummary = styles.summary; - let widthSummary = stylesSummary['width']; - expect(widthSummary.prop).toEqual('width'); - expect(widthSummary.value).toEqual('100px'); - - let heightSummary = stylesSummary['height']; - expect(heightSummary.prop).toEqual('height'); - expect(heightSummary.value).toEqual('200px'); - - let classesSummary = classes.summary; - let abcSummary = classesSummary['abc']; - expect(abcSummary.prop).toEqual('abc'); - expect(abcSummary.value).toBeTruthy(); + expectStyle(element).toEqual({width: '100px', height: '200px'}); + expectClass(element).toEqual({abc: true}); comp.reset(); comp.updateStyles('width', '500px'); @@ -1200,23 +1585,8 @@ describe('styling', () => { comp.updateClasses('def'); fixture.detectChanges(); - styles = node.styles !; - classes = node.classes !; - - stylesSummary = styles.summary; - widthSummary = stylesSummary['width']; - expect(widthSummary.value).toEqual('500px'); - - heightSummary = stylesSummary['height']; - expect(heightSummary.value).toEqual(null); - - classesSummary = classes.summary; - abcSummary = classesSummary['abc']; - expect(abcSummary).toBeUndefined(); - - let defSummary = classesSummary['def']; - expect(defSummary.prop).toEqual('def'); - expect(defSummary.value).toBeTruthy(); + expectStyle(element).toEqual({width: '500px'}); + expectClass(element).toEqual({def: true}); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -1230,16 +1600,16 @@ describe('styling', () => { @Component({ template: `
+ [style]="map" + style="width:200px; font-size:99px" + dir-that-sets-styling + #dir + [class.xyz]="xyz">
` }) class Cmp { map: any = {width: '111px', opacity: '0.5'}; - width: string|null = '555px'; + width: string|null|undefined = '555px'; @ViewChild('dir', {read: DirThatSetsStyling, static: true}) dir !: DirThatSetsStyling; @@ -1251,21 +1621,17 @@ describe('styling', () => { fixture.detectChanges(); const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - - const styles = node.styles !; - - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '555px', 'color': 'red', 'font-size': '99px', 'opacity': '0.5', }); - comp.width = null; + comp.width = undefined; fixture.detectChanges(); - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '111px', 'color': 'red', 'font-size': '99px', @@ -1275,21 +1641,18 @@ describe('styling', () => { comp.map = null; fixture.detectChanges(); - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '777px', 'color': 'red', 'font-size': '99px', - 'opacity': null, }); comp.dir.map = null; fixture.detectChanges(); - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '200px', - 'color': null, 'font-size': '99px', - 'opacity': null, }); }); @@ -1312,8 +1675,8 @@ describe('styling', () => { ` }) class Cmp { - width: string|null = '111px'; - height: string|null = '111px'; + width: string|null|undefined = '111px'; + height: string|null|undefined = '111px'; map: any = {width: '555px', height: '555px'}; @@ -1329,8 +1692,7 @@ describe('styling', () => { fixture.detectChanges(); const element = fixture.nativeElement.querySelector('div'); - // both are applied because this is the first pass - assertStyleCounters(2, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '111px'); assertStyle(element, 'height', '111px'); @@ -1350,7 +1712,7 @@ describe('styling', () => { assertStyle(element, 'width', '222px'); assertStyle(element, 'height', '222px'); - comp.width = null; + comp.width = undefined; ngDevModeResetPerfCounters(); fixture.detectChanges(); @@ -1370,17 +1732,16 @@ describe('styling', () => { ngDevModeResetPerfCounters(); fixture.detectChanges(); - // both are applied because the map was altered - assertStyleCounters(2, 0); + // No change, hence no write + assertStyleCounters(0, 0); assertStyle(element, 'width', '123px'); assertStyle(element, 'height', '123px'); - comp.width = null; + comp.width = undefined; ngDevModeResetPerfCounters(); fixture.detectChanges(); - // the width is applied both in TEMPLATE and in HOST_BINDINGS mode - assertStyleCounters(2, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '999px'); assertStyle(element, 'height', '123px'); @@ -1397,19 +1758,18 @@ describe('styling', () => { ngDevModeResetPerfCounters(); fixture.detectChanges(); - // only the width and color have changed - assertStyleCounters(2, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '1000px'); assertStyle(element, 'height', '123px'); assertStyle(element, 'color', 'red'); - comp.height = null; + comp.height = undefined; ngDevModeResetPerfCounters(); fixture.detectChanges(); // height gets applied twice and all other // values get applied - assertStyleCounters(4, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '1000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'red'); @@ -1418,7 +1778,7 @@ describe('styling', () => { ngDevModeResetPerfCounters(); fixture.detectChanges(); - assertStyleCounters(5, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '2000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'blue'); @@ -1429,20 +1789,21 @@ describe('styling', () => { fixture.detectChanges(); // all four are applied because the map was altered - assertStyleCounters(4, 1); + // TODO: temporary dissable as it fails in IE. Re-enabled in #34804 + // assertStyleCounters(1, 0); assertStyle(element, 'width', '2000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'blue'); assertStyle(element, 'opacity', ''); }); - onlyInIvy('only ivy has style/class bindings debugging support') + onlyInIvy('only ivy has [style] support') .it('should sanitize style values before writing them', () => { @Component({ template: `
+ [style.background-image]="bgImageExp" + [style]="styleMapExp">
` }) class Cmp { @@ -1456,92 +1817,63 @@ describe('styling', () => { const comp = fixture.componentInstance; fixture.detectChanges(); - const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; + const div = fixture.nativeElement.querySelector('div'); - const lastSanitizedProps: any[] = []; - styles.overrideSanitizer((prop, value) => { - lastSanitizedProps.push(prop); - return value; - }); - - comp.bgImageExp = '123'; + comp.bgImageExp = 'url("javascript:img")'; fixture.detectChanges(); + // for some reasons `background-image: unsafe` is suppressed + expect(getSortedStyle(div)).toEqual(''); - expect(styles.values).toEqual({ - 'background-image': '123', - 'width': null, - }); - - expect(lastSanitizedProps).toEqual(['background-image']); - lastSanitizedProps.length = 0; - - comp.styleMapExp = {'clip-path': '456'}; + // for some reasons `border-image: unsafe` is NOT suppressed + comp.styleMapExp = {'filter': 'url("javascript:border")'}; fixture.detectChanges(); + expect(getSortedStyle(div)).not.toContain('javascript'); - expect(styles.values).toEqual({ - 'background-image': '123', - 'clip-path': '456', - 'width': null, - }); - - expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); - lastSanitizedProps.length = 0; - + // Prove that bindings work. comp.widthExp = '789px'; + comp.bgImageExp = bypassSanitizationTrustStyle(comp.bgImageExp) as string; + comp.styleMapExp = { + 'filter': bypassSanitizationTrustStyle(comp.styleMapExp['filter']) as string + }; fixture.detectChanges(); - expect(styles.values).toEqual({ - 'background-image': '123', - 'clip-path': '456', - 'width': '789px', - }); - - expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); - lastSanitizedProps.length = 0; + expect(div.style.getPropertyValue('background-image')).toEqual('url("javascript:img")'); + // Some browsers strip `url` on filter so we use `toContain` + expect(div.style.getPropertyValue('filter')).toContain('javascript:border'); + expect(div.style.getPropertyValue('width')).toEqual('789px'); }); - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should apply a unit to a style before writing it', () => { - @Component({ - template: ` + it('should apply a unit to a style before writing it', () => { + @Component({ + template: `
+ [style.height.em]="heightExp">
` - }) - class Cmp { - widthExp: string|number|null = ''; - heightExp: string|number|null = ''; - } + }) + class Cmp { + widthExp: string|number|null = ''; + heightExp: string|number|null = ''; + } - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - fixture.detectChanges(); + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); - const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; + const div = fixture.nativeElement.querySelector('div'); - comp.widthExp = '200'; - comp.heightExp = 10; - fixture.detectChanges(); + comp.widthExp = '200'; + comp.heightExp = 10; + fixture.detectChanges(); - expect(styles.values).toEqual({ - 'width': '200px', - 'height': '10em', - }); + expect(getSortedStyle(div)).toEqual('height: 10em; width: 200px;'); - comp.widthExp = 0; - comp.heightExp = null; - fixture.detectChanges(); + comp.widthExp = 0; + comp.heightExp = null; + fixture.detectChanges(); - expect(styles.values).toEqual({ - 'width': '0px', - 'height': null, - }); - }); + expect(getSortedStyle(div)).toEqual('width: 0px;'); + }); it('should be able to bind a SafeValue to clip-path', () => { @Component({template: '
'}) @@ -1657,15 +1989,10 @@ describe('styling', () => { fixture.detectChanges(); const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; - - const values = styles.values; - const props = Object.keys(values).sort(); - expect(props).toEqual(['color', 'width']); - - expect(values['width']).toEqual('200px'); - expect(values['color']).toEqual('red'); + expectStyle(element).toEqual({ + color: 'red', + width: '200px', + }); }); onlyInIvy('only ivy has style/class bindings debugging support') @@ -2350,12 +2677,12 @@ describe('styling', () => { ` }) class Cmp { - style: any = {width: '100px'}; - klass: any = {foo: true, bar: false}; + style: any = 'width: 100px'; + klass: any = 'foo'; ngAfterViewInit() { - this.style = {height: '200px'}; - this.klass = {foo: false}; + this.style = 'height: 200px'; + this.klass = 'bar'; } } @@ -2486,25 +2813,393 @@ describe('styling', () => { expect(getComputedStyle(div).width).toBe('10px'); }); - it('should allow classes with trailing and leading spaces in [ngClass]', () => { - @Component({ - template: ` -
-
- ` - }) - class Cmp { - applyClasses = true; + onlyInIvy('[style] binding is supported in Ivy only') + .it('should allow multiple styling bindings to work alongside property/attribute bindings', + () => { + @Component({ + template: ` +
+
` + }) + class MyComp { + } + + @Directive({selector: '[dir-that-sets-styles]'}) + class DirThatSetsStyling { + @HostBinding('style.width') public w = '100px'; + @HostBinding('style.height') public h = '200px'; + } + + const fixture = + TestBed.configureTestingModule({declarations: [MyComp, DirThatSetsStyling]}) + .createComponent(MyComp); + fixture.detectChanges(); + const div = fixture.nativeElement.querySelector('div') !; + expect(div.style.getPropertyValue('width')).toEqual('100px'); + expect(div.style.getPropertyValue('height')).toEqual('200px'); + expect(div.style.getPropertyValue('font-size')).toEqual('300px'); + expect(div.getAttribute('title')).toEqual('my-title'); + expect(div.getAttribute('data-foo')).toEqual('my-foo'); + }); + + onlyInIvy('VE clobers in case of @HostBinding("class")') + .it('should allow host styling on the root element with external styling', () => { + @Component({template: '...'}) + class MyComp { + @HostBinding('class') public classes = ''; + } + + const fixture = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + fixture.detectChanges(); + const root = fixture.nativeElement as HTMLElement; + expect(root.className).toEqual(''); + + fixture.componentInstance.classes = '1 2 3'; + fixture.detectChanges(); + expect(root.className.split(/\s+/).sort().join(' ')).toEqual('1 2 3'); + + root.classList.add('0'); + expect(root.className.split(/\s+/).sort().join(' ')).toEqual('0 1 2 3'); + + fixture.componentInstance.classes = '1 2 3 4'; + fixture.detectChanges(); + expect(root.className.split(/\s+/).sort().join(' ')).toEqual('0 1 2 3 4'); + }); + + it('should apply camelCased class names', () => { + @Component({template: `
`}) + class MyComp { } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({ + declarations: [MyComp], + }); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const classList = (fixture.nativeElement.querySelector('div') as HTMLDivElement).classList; + expect(classList.contains('fooBar')).toBeTruthy(); + expect(classList.contains('barFoo')).toBeTruthy(); + }); + + // onlyInIvy('[style] bindings are ivy only') + xit('should convert camelCased style property names to snake-case', () => { + // TODO(misko): Temporarily disabled in this PR renabled in + // https://github.com/angular/angular/pull/34616 + // Current implementation uses strings to write to DOM. Because of that it does not convert + // property names from camelCase to dash-case. This is rectified in #34616 because we switch + // from string API to `element.style.setProperty` API. + @Component({template: `
`}) + class MyComp { + myStyles = {}; + } + + TestBed.configureTestingModule({ + declarations: [MyComp], + }); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div') as HTMLDivElement; + fixture.componentInstance.myStyles = {fontSize: '200px'}; + fixture.detectChanges(); + + expect(div.style.getPropertyValue('font-size')).toEqual('200px'); + }); + + it('should recover from an error thrown in styling bindings', () => { + let raiseWidthError = false; + + @Component({template: `
`}) + class MyComp { + get myWidth() { + if (raiseWidthError) { + throw new Error('...'); + } + return '100px'; + } + } + + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + + raiseWidthError = true; + expect(() => fixture.detectChanges()).toThrow(); + + raiseWidthError = false; + fixture.detectChanges(); + const div = fixture.nativeElement.querySelector('div') as HTMLDivElement; + expect(div.style.getPropertyValue('width')).toEqual('100px'); + expect(div.style.getPropertyValue('height')).toEqual('200px'); + }); + + onlyInIvy('Prioritization works in Ivy only') + .it('should prioritize host bindings for templates first, then directives and finally components', + () => { + @Component({selector: 'my-comp-with-styling', template: ''}) + class MyCompWithStyling { + @HostBinding('style') + myStyles: any = {width: '300px'}; + + @HostBinding('style.height') + myHeight: any = '305px'; + } + + @Directive({selector: '[my-dir-with-styling]'}) + class MyDirWithStyling { + @HostBinding('style') + myStyles: any = {width: '200px'}; + + @HostBinding('style.height') + myHeight: any = '205px'; + } + + @Component({ + template: ` + + + ` + }) + class MyComp { + myStyles: {width?: string} = {width: '100px'}; + myHeight: string|null|undefined = '100px'; + + @ViewChild(MyDirWithStyling) dir !: MyDirWithStyling; + @ViewChild(MyCompWithStyling) comp !: MyCompWithStyling; + } + + TestBed.configureTestingModule( + {declarations: [MyComp, MyCompWithStyling, MyDirWithStyling]}); + const fixture = TestBed.createComponent(MyComp); + const comp = fixture.componentInstance; + const elm = fixture.nativeElement.querySelector('my-comp-with-styling') !; + + fixture.detectChanges(); + expect(elm.style.width).toEqual('100px'); + expect(elm.style.height).toEqual('100px'); + + comp.myStyles = {}; + comp.myHeight = undefined; + fixture.detectChanges(); + expect(elm.style.width).toEqual('200px'); + expect(elm.style.height).toEqual('205px'); + + comp.dir.myStyles = {}; + comp.dir.myHeight = undefined; + fixture.detectChanges(); + expect(elm.style.width).toEqual('300px'); + expect(elm.style.height).toEqual('305px'); + + comp.comp.myStyles = {}; + comp.comp.myHeight = undefined; + fixture.detectChanges(); + expect(elm.style.width).toEqual('1px'); + expect(elm.style.height).toEqual('1px'); + }); + + it('should combine host class.foo bindings from multiple directives', () => { + + @Directive({ + selector: '[dir-that-sets-one-two]', + exportAs: 'one', + }) + class DirThatSetsOneTwo { + @HostBinding('class.one') one = false; + @HostBinding('class.two') two = false; + } + + @Directive({ + selector: '[dir-that-sets-three-four]', + exportAs: 'two', + }) + class DirThatSetsThreeFour { + @HostBinding('class.three') three = false; + @HostBinding('class.four') four = false; + } + + @Component({ + template: ` +
+
+ ` + }) + class MyComp { + @ViewChild('div1', {static: true, read: DirThatSetsOneTwo}) + public dirOneA: DirThatSetsOneTwo|null = null; + + @ViewChild('div1', {static: true, read: DirThatSetsThreeFour}) + public dirTwoA: DirThatSetsThreeFour|null = null; + + @ViewChild('div2', {static: true, read: DirThatSetsOneTwo}) + public dirOneB: DirThatSetsOneTwo|null = null; + + @ViewChild('div2', {static: true, read: DirThatSetsThreeFour}) + public dirTwoB: DirThatSetsThreeFour|null = null; + + zero = false; + } + + TestBed.configureTestingModule( + {declarations: [MyComp, DirThatSetsThreeFour, DirThatSetsOneTwo]}); + + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const [div1, div2] = fixture.nativeElement.querySelectorAll('div') as HTMLDivElement[]; + + expect(div1.className).toBe(''); + expect(div2.className).toBe(''); + + const comp = fixture.componentInstance; + comp.dirOneA !.one = comp.dirOneB !.one = true; + comp.dirOneA !.two = comp.dirOneB !.two = true; + fixture.detectChanges(); + + expect(div1.classList.contains('one')).toBeTruthy(); + expect(div1.classList.contains('two')).toBeTruthy(); + expect(div1.classList.contains('three')).toBeFalsy(); + expect(div1.classList.contains('four')).toBeFalsy(); + expect(div2.classList.contains('one')).toBeTruthy(); + expect(div2.classList.contains('two')).toBeTruthy(); + expect(div2.classList.contains('three')).toBeFalsy(); + expect(div2.classList.contains('four')).toBeFalsy(); + expect(div2.classList.contains('zero')).toBeFalsy(); + + comp.dirTwoA !.three = comp.dirTwoB !.three = true; + comp.dirTwoA !.four = comp.dirTwoB !.four = true; + fixture.detectChanges(); + + expect(div1.classList.contains('one')).toBeTruthy(); + expect(div1.classList.contains('two')).toBeTruthy(); + expect(div1.classList.contains('three')).toBeTruthy(); + expect(div1.classList.contains('four')).toBeTruthy(); + expect(div2.classList.contains('one')).toBeTruthy(); + expect(div2.classList.contains('two')).toBeTruthy(); + expect(div2.classList.contains('three')).toBeTruthy(); + expect(div2.classList.contains('four')).toBeTruthy(); + expect(div2.classList.contains('zero')).toBeFalsy(); + + comp.zero = true; + fixture.detectChanges(); + + expect(div1.classList.contains('one')).toBeTruthy(); + expect(div1.classList.contains('two')).toBeTruthy(); + expect(div1.classList.contains('three')).toBeTruthy(); + expect(div1.classList.contains('four')).toBeTruthy(); + expect(div2.classList.contains('one')).toBeTruthy(); + expect(div2.classList.contains('two')).toBeTruthy(); + expect(div2.classList.contains('three')).toBeTruthy(); + expect(div2.classList.contains('four')).toBeTruthy(); + expect(div2.classList.contains('zero')).toBeTruthy(); + }); + + it('should combine static host classes with component "class" host attribute', () => { + @Component({selector: 'comp-with-classes', template: '', host: {'class': 'host'}}) + class CompWithClasses { + constructor(ref: ElementRef) { ref.nativeElement.classList.add('custom'); } + } + + @Component({ + template: `` + }) + class MyComp { + items = [1, 2, 3]; + } + + const fixture = TestBed + .configureTestingModule({ + declarations: [MyComp, CompWithClasses], + }) + .createComponent(MyComp); + fixture.detectChanges(); + + const [one, two, three] = + fixture.nativeElement.querySelectorAll('comp-with-classes') as HTMLDivElement[]; + + expect(one.classList.contains('custom')).toBeTruthy(); + expect(one.classList.contains('inline')).toBeTruthy(); + expect(one.classList.contains('host')).toBeTruthy(); + + expect(two.classList.contains('custom')).toBeTruthy(); + expect(two.classList.contains('inline')).toBeTruthy(); + expect(two.classList.contains('host')).toBeTruthy(); + + expect(three.classList.contains('custom')).toBeTruthy(); + expect(three.classList.contains('inline')).toBeTruthy(); + expect(three.classList.contains('host')).toBeTruthy(); + }); + + it('should allow a single style host binding on an element', () => { + @Component({template: `
`}) + class Cmp { + } + + @Directive({selector: '[single-host-style-dir]'}) + class SingleHostStyleDir { + @HostBinding('style.width') + width = '100px'; + } + + TestBed.configureTestingModule({declarations: [Cmp, SingleHostStyleDir]}); const fixture = TestBed.createComponent(Cmp); fixture.detectChanges(); - const leading = fixture.nativeElement.querySelector('[leading-space]'); - const trailing = fixture.nativeElement.querySelector('[trailing-space]'); - expect(leading.className).toBe('foo', 'Expected class to be applied despite leading space.'); - expect(trailing.className).toBe('foo', 'Expected class to be applied despite trailing space.'); + const element = fixture.nativeElement.querySelector('div'); + expect(element.style.width).toEqual('100px'); + }); + + it('should override class bindings when a directive extends another directive', () => { + @Component({template: ``}) + class Cmp { + } + + @Component({ + selector: 'parent-comp', + host: {'class': 'parent-comp', '[class.parent-comp-active]': 'true'}, + template: '...', + }) + class ParentComp { + } + + @Component({ + selector: 'child-comp', + host: { + 'class': 'child-comp', + '[class.child-comp-active]': 'true', + '[class.parent-comp]': 'false', + '[class.parent-comp-active]': 'false' + }, + template: '...', + }) + class ChildComp extends ParentComp { + } + + TestBed.configureTestingModule({declarations: [Cmp, ChildComp, ParentComp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('child-comp'); + expect(element.classList.contains('template')).toBeTruthy(); + + expect(element.classList.contains('child-comp')).toBeTruthy(); + expect(element.classList.contains('child-comp-active')).toBeTruthy(); + + expect(element.classList.contains('parent-comp')).toBeFalsy(); + expect(element.classList.contains('parent-comp-active')).toBeFalsy(); }); // TODO(FW-1360): re-enable this test once the new styling changes are in place. @@ -2530,7 +3225,6 @@ describe('styling', () => { expect(logs).toEqual([]); }); - }); function assertStyleCounters(countForSet: number, countForRemove: number) { @@ -2541,3 +3235,11 @@ function assertStyleCounters(countForSet: number, countForRemove: number) { function assertStyle(element: HTMLElement, prop: string, value: any) { expect((element.style as any)[prop]).toEqual(value); } + +function expectStyle(element: HTMLElement) { + return expect(getElementStyles(element)); +} + +function expectClass(element: HTMLElement) { + return expect(getElementClasses(element)); +} \ No newline at end of file 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 dfd99b843c..b56ac936cc 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -113,9 +113,6 @@ { "name": "RENDERER_FACTORY" }, - { - "name": "RendererStyleFlags3" - }, { "name": "SANITIZER" }, @@ -158,18 +155,12 @@ { "name": "addHostBindingsToExpandoInstructions" }, - { - "name": "addItemToStylingMap" - }, { "name": "addToViewTree" }, { "name": "allocLFrame" }, - { - "name": "allocStylingMapArray" - }, { "name": "appendChild" }, @@ -195,7 +186,13 @@ "name": "classIndexOf" }, { - "name": "concatString" + "name": "clearActiveHostElement" + }, + { + "name": "computeStaticStyling" + }, + { + "name": "concatStringsWithSpace" }, { "name": "createDirectivesInstances" @@ -284,9 +281,6 @@ { "name": "findDirectiveDefMatches" }, - { - "name": "forceStylesAsString" - }, { "name": "generateExpandoInstructionBlock" }, @@ -335,9 +329,6 @@ { "name": "getFirstNativeNode" }, - { - "name": "getInitialStylingValue" - }, { "name": "getInjectorIndex" }, @@ -353,12 +344,6 @@ { "name": "getLViewParent" }, - { - "name": "getMapProp" - }, - { - "name": "getMapValue" - }, { "name": "getNameOnlyMarkerIndex" }, @@ -407,15 +392,9 @@ { "name": "getSelectedIndex" }, - { - "name": "getStylingMapArray" - }, { "name": "growHostVarsSpace" }, - { - "name": "hasActiveElementFlag" - }, { "name": "hasClassInput" }, @@ -428,18 +407,12 @@ { "name": "hasTagAndTypeMatch" }, - { - "name": "hyphenate" - }, { "name": "includeViewProviders" }, { "name": "increaseElementDepthCount" }, - { - "name": "incrementActiveDirectiveId" - }, { "name": "incrementInitPhaseFlags" }, @@ -509,12 +482,6 @@ { "name": "isProceduralRenderer" }, - { - "name": "isStylingContext" - }, - { - "name": "isStylingValueDefined" - }, { "name": "leaveDI" }, @@ -557,12 +524,6 @@ { "name": "noSideEffects" }, - { - "name": "objectToClassName" - }, - { - "name": "setHostBindingsByExecutingExpandoInstructions" - }, { "name": "refreshChildComponents" }, @@ -581,9 +542,6 @@ { "name": "refreshView" }, - { - "name": "registerInitialStylingOnTNode" - }, { "name": "registerPostOrderHooks" }, @@ -599,15 +557,9 @@ { "name": "renderComponent" }, - { - "name": "renderInitialStyling" - }, { "name": "renderStringify" }, - { - "name": "renderStylingMap" - }, { "name": "renderView" }, @@ -623,9 +575,6 @@ { "name": "saveResolvedLocalsInData" }, - { - "name": "selectClassBasedInputName" - }, { "name": "selectIndexInternal" }, @@ -638,17 +587,14 @@ { "name": "setBindingRoot" }, - { - "name": "setClass" - }, - { - "name": "setClassName" - }, { "name": "setCurrentQueryIndex" }, { - "name": "setDirectiveStylingInput" + "name": "setDirectiveInputsWhichShadowsStyling" + }, + { + "name": "setHostBindingsByExecutingExpandoInstructions" }, { "name": "setIncludeViewProviders" @@ -665,30 +611,18 @@ { "name": "setIsNotParent" }, - { - "name": "setMapValue" - }, { "name": "setPreviousOrParentTNode" }, { "name": "setSelectedIndex" }, - { - "name": "setStyle" - }, - { - "name": "setStyleAttr" - }, { "name": "setUpAttributes" }, { "name": "stringifyForError" }, - { - "name": "stylingMapToString" - }, { "name": "syncViewWithBlueprint" }, @@ -698,14 +632,14 @@ { "name": "unwrapRNode" }, - { - "name": "updateRawValueOnContext" - }, { "name": "viewAttachedToChangeDetector" }, { - "name": "writeStylingValueDirectly" + "name": "writeDirectClass" + }, + { + "name": "writeDirectStyle" }, { "name": "ɵɵdefineComponent" 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 54896c13b1..6c68029791 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -104,9 +104,6 @@ { "name": "RENDERER_FACTORY" }, - { - "name": "RendererStyleFlags3" - }, { "name": "SANITIZER" }, @@ -143,18 +140,12 @@ { "name": "addHostBindingsToExpandoInstructions" }, - { - "name": "addItemToStylingMap" - }, { "name": "addToViewTree" }, { "name": "allocLFrame" }, - { - "name": "allocStylingMapArray" - }, { "name": "appendChild" }, @@ -174,7 +165,13 @@ "name": "callHooks" }, { - "name": "concatString" + "name": "clearActiveHostElement" + }, + { + "name": "computeStaticStyling" + }, + { + "name": "concatStringsWithSpace" }, { "name": "createLFrame" @@ -242,9 +239,6 @@ { "name": "extractPipeDef" }, - { - "name": "forceStylesAsString" - }, { "name": "generateExpandoInstructionBlock" }, @@ -278,9 +272,6 @@ { "name": "getFirstNativeNode" }, - { - "name": "getInitialStylingValue" - }, { "name": "getInjectorIndex" }, @@ -296,12 +287,6 @@ { "name": "getLViewParent" }, - { - "name": "getMapProp" - }, - { - "name": "getMapValue" - }, { "name": "getNativeAnchorNode" }, @@ -344,27 +329,15 @@ { "name": "getSelectedIndex" }, - { - "name": "getStylingMapArray" - }, { "name": "growHostVarsSpace" }, - { - "name": "hasActiveElementFlag" - }, { "name": "hasParentInjector" }, - { - "name": "hyphenate" - }, { "name": "includeViewProviders" }, - { - "name": "incrementActiveDirectiveId" - }, { "name": "incrementInitPhaseFlags" }, @@ -404,12 +377,6 @@ { "name": "isProceduralRenderer" }, - { - "name": "isStylingContext" - }, - { - "name": "isStylingValueDefined" - }, { "name": "leaveDI" }, @@ -443,12 +410,6 @@ { "name": "noSideEffects" }, - { - "name": "objectToClassName" - }, - { - "name": "setHostBindingsByExecutingExpandoInstructions" - }, { "name": "refreshChildComponents" }, @@ -467,9 +428,6 @@ { "name": "refreshView" }, - { - "name": "registerInitialStylingOnTNode" - }, { "name": "registerPreOrderHooks" }, @@ -482,15 +440,9 @@ { "name": "renderComponent" }, - { - "name": "renderInitialStyling" - }, { "name": "renderStringify" }, - { - "name": "renderStylingMap" - }, { "name": "renderView" }, @@ -509,59 +461,44 @@ { "name": "setBindingRoot" }, - { - "name": "setClass" - }, - { - "name": "setClassName" - }, { "name": "setCurrentQueryIndex" }, + { + "name": "setHostBindingsByExecutingExpandoInstructions" + }, { "name": "setIncludeViewProviders" }, { "name": "setInjectImplementation" }, - { - "name": "setMapValue" - }, { "name": "setPreviousOrParentTNode" }, { "name": "setSelectedIndex" }, - { - "name": "setStyle" - }, - { - "name": "setStyleAttr" - }, { "name": "setUpAttributes" }, { "name": "stringifyForError" }, - { - "name": "stylingMapToString" - }, { "name": "syncViewWithBlueprint" }, { "name": "unwrapRNode" }, - { - "name": "updateRawValueOnContext" - }, { "name": "viewAttachedToChangeDetector" }, { - "name": "writeStylingValueDirectly" + "name": "writeDirectClass" + }, + { + "name": "writeDirectStyle" }, { "name": "ɵɵdefineComponent" diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 8f3e88c04d..3917b6be2c 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -2,9 +2,6 @@ { "name": "ACTIVE_INDEX" }, - { - "name": "BIT_MASK_START_VALUE" - }, { "name": "BLOOM_MASK" }, @@ -38,18 +35,6 @@ { "name": "DECLARATION_VIEW" }, - { - "name": "DEFAULT_BINDING_INDEX" - }, - { - "name": "DEFAULT_BINDING_VALUE" - }, - { - "name": "DEFAULT_GUARD_MASK_VALUE" - }, - { - "name": "DEFAULT_TOTAL_SOURCES" - }, { "name": "DOCUMENT" }, @@ -90,7 +75,7 @@ "name": "HOST" }, { - "name": "INDEX_START_VALUE" + "name": "IGNORE_DUE_TO_INPUT_SHADOW" }, { "name": "INJECTOR" @@ -107,12 +92,6 @@ { "name": "IterableDiffers" }, - { - "name": "MAP_BASED_ENTRY_PROP_NAME" - }, - { - "name": "MAP_DIRTY_VALUE" - }, { "name": "MONKEY_PATCH_KEY_NAME" }, @@ -206,15 +185,9 @@ { "name": "RecordViewTuple" }, - { - "name": "RendererStyleFlags3" - }, { "name": "SANITIZER" }, - { - "name": "STYLING_INDEX_FOR_MAP_BINDING" - }, { "name": "SWITCH_ELEMENT_REF_FACTORY" }, @@ -230,9 +203,6 @@ { "name": "SkipSelf" }, - { - "name": "TEMPLATE_DIRECTIVE_INDEX" - }, { "name": "TNODE" }, @@ -317,9 +287,6 @@ { "name": "__window" }, - { - "name": "_activeStylingMapApplyFn" - }, { "name": "_currentInjector" }, @@ -332,27 +299,15 @@ { "name": "_renderCompCount" }, - { - "name": "_state" - }, { "name": "_symbolIterator" }, - { - "name": "addBindingIntoContext" - }, { "name": "addComponentLogic" }, { "name": "addHostBindingsToExpandoInstructions" }, - { - "name": "addItemToStylingMap" - }, - { - "name": "addNewSourceColumn" - }, { "name": "addRemoveViewFromContainer" }, @@ -365,21 +320,12 @@ { "name": "allocLFrame" }, - { - "name": "allocStylingMapArray" - }, - { - "name": "allocTStylingContext" - }, - { - "name": "allocateNewContextEntry" - }, - { - "name": "allowDirectStyling" - }, { "name": "appendChild" }, + { + "name": "appendStyling" + }, { "name": "applyContainer" }, @@ -389,15 +335,6 @@ { "name": "applyProjectionRecursive" }, - { - "name": "applyStylingValue" - }, - { - "name": "applyStylingValueDirectly" - }, - { - "name": "applyStylingViaContext" - }, { "name": "applyToElementOrContainer" }, @@ -437,17 +374,50 @@ { "name": "checkNoChangesInternal" }, + { + "name": "checkStylingProperty" + }, { "name": "classIndexOf" }, { "name": "cleanUpView" }, + { + "name": "clearActiveHostElement" + }, { "name": "collectNativeNodes" }, { - "name": "concatString" + "name": "computeClassChanges" + }, + { + "name": "computeStaticStyling" + }, + { + "name": "computeStyleChanges" + }, + { + "name": "concatStringsWithSpace" + }, + { + "name": "consumeClassToken" + }, + { + "name": "consumeQuotedText" + }, + { + "name": "consumeSeparator" + }, + { + "name": "consumeStyleKey" + }, + { + "name": "consumeStyleValue" + }, + { + "name": "consumeWhitespace" }, { "name": "createContainerRef" @@ -575,9 +545,6 @@ { "name": "extractPipeDef" }, - { - "name": "findAndApplyMapValue" - }, { "name": "findAttrIndexInNode" }, @@ -591,10 +558,10 @@ "name": "findViaComponent" }, { - "name": "flushStyling" + "name": "flushStyleBinding" }, { - "name": "forceStylesAsString" + "name": "flushStylingOnElementExit" }, { "name": "forwardRef" @@ -608,15 +575,9 @@ { "name": "generatePropertyAliases" }, - { - "name": "getActiveDirectiveId" - }, { "name": "getBeforeNodeForView" }, - { - "name": "getBindingValue" - }, { "name": "getBindingsEnabled" }, @@ -624,7 +585,7 @@ "name": "getCheckNoChangesMode" }, { - "name": "getClassesContext" + "name": "getClassBindingChanged" }, { "name": "getCleanup" @@ -647,9 +608,6 @@ { "name": "getContainerRenderParent" }, - { - "name": "getContext" - }, { "name": "getContextLView" }, @@ -659,9 +617,6 @@ { "name": "getDebugContext" }, - { - "name": "getDefaultValue" - }, { "name": "getDirectiveDef" }, @@ -680,12 +635,6 @@ { "name": "getFirstNativeNode" }, - { - "name": "getGuardMask" - }, - { - "name": "getInitialStylingValue" - }, { "name": "getInjectableDef" }, @@ -705,10 +654,10 @@ "name": "getLViewParent" }, { - "name": "getMapProp" + "name": "getLastParsedKey" }, { - "name": "getMapValue" + "name": "getLastParsedValue" }, { "name": "getNameOnlyMarkerIndex" @@ -773,35 +722,14 @@ { "name": "getPreviousOrParentTNode" }, - { - "name": "getProp" - }, - { - "name": "getPropConfig" - }, - { - "name": "getPropValuesStartPosition" - }, { "name": "getRenderParent" }, - { - "name": "getRenderer" - }, { "name": "getSelectedIndex" }, { - "name": "getStylesContext" - }, - { - "name": "getStylingMapArray" - }, - { - "name": "getStylingMapsSyncFn" - }, - { - "name": "getStylingState" + "name": "getStyleBindingChanged" }, { "name": "getSymbolIterator" @@ -810,10 +738,19 @@ "name": "getTNode" }, { - "name": "getTViewCleanup" + "name": "getTStylingRangeNext" }, { - "name": "getTotalSources" + "name": "getTStylingRangePrev" + }, + { + "name": "getTStylingRangePrevDuplicate" + }, + { + "name": "getTStylingRangeTail" + }, + { + "name": "getTViewCleanup" }, { "name": "getTypeName" @@ -821,42 +758,27 @@ { "name": "getTypeNameForDebugging" }, - { - "name": "getValue" - }, - { - "name": "getValuesCount" - }, { "name": "growHostVarsSpace" }, { "name": "handleError" }, - { - "name": "hasActiveElementFlag" - }, { "name": "hasClassInput" }, - { - "name": "hasConfig" - }, { "name": "hasParentInjector" }, { "name": "hasStyleInput" }, + { + "name": "hasStylingInputShadow" + }, { "name": "hasTagAndTypeMatch" }, - { - "name": "hasValueChanged" - }, - { - "name": "hyphenate" - }, { "name": "includeViewProviders" }, @@ -864,7 +786,7 @@ "name": "increaseElementDepthCount" }, { - "name": "incrementActiveDirectiveId" + "name": "incrementBindingIndex" }, { "name": "incrementInitPhaseFlags" @@ -893,6 +815,9 @@ { "name": "insertBloom" }, + { + "name": "insertTStylingBinding" + }, { "name": "insertView" }, @@ -917,6 +842,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isActiveHostElement" + }, { "name": "isAnimationProp" }, @@ -948,10 +876,7 @@ "name": "isForwardRef" }, { - "name": "isHostStyling" - }, - { - "name": "isHostStylingActive" + "name": "isInHostBindings" }, { "name": "isJsObject" @@ -983,15 +908,6 @@ { "name": "isRootView" }, - { - "name": "isSanitizationRequired" - }, - { - "name": "isStylingContext" - }, - { - "name": "isStylingValueDefined" - }, { "name": "iterateListLike" }, @@ -1037,6 +953,12 @@ { "name": "markDirtyIfOnPush" }, + { + "name": "markDuplicates" + }, + { + "name": "markStylingBindingDirty" + }, { "name": "markViewDirty" }, @@ -1083,19 +1005,28 @@ "name": "noSideEffects" }, { - "name": "normalizeBitMaskValue" + "name": "parseClassName" }, { - "name": "objectToClassName" + "name": "parseClassNameNext" }, { - "name": "patchConfig" + "name": "parseKeyValue" }, { - "name": "patchHostStylingFlag" + "name": "parseStyle" }, { - "name": "setHostBindingsByExecutingExpandoInstructions" + "name": "parseStyleNext" + }, + { + "name": "parserState" + }, + { + "name": "processClassToken" + }, + { + "name": "processStyleKeyValue" }, { "name": "readPatchedData" @@ -1103,6 +1034,12 @@ { "name": "readPatchedLView" }, + { + "name": "reconcileClassNames" + }, + { + "name": "reconcileStyleNames" + }, { "name": "refreshChildComponents" }, @@ -1121,12 +1058,6 @@ { "name": "refreshView" }, - { - "name": "registerBinding" - }, - { - "name": "registerInitialStylingOnTNode" - }, { "name": "registerPostOrderHooks" }, @@ -1139,6 +1070,9 @@ { "name": "removeListeners" }, + { + "name": "removeStyle" + }, { "name": "removeView" }, @@ -1157,30 +1091,18 @@ { "name": "renderDetachView" }, - { - "name": "renderHostBindingsAsStale" - }, - { - "name": "renderInitialStyling" - }, { "name": "renderStringify" }, - { - "name": "renderStylingMap" - }, { "name": "renderView" }, { - "name": "resetCurrentStyleSanitizer" + "name": "resetParserState" }, { "name": "resetPreOrderHookFlags" }, - { - "name": "resetStylingState" - }, { "name": "resolveDirectives" }, @@ -1199,15 +1121,9 @@ { "name": "searchTokensOnInjector" }, - { - "name": "selectClassBasedInputName" - }, { "name": "selectIndexInternal" }, - { - "name": "setActiveElementFlag" - }, { "name": "setActiveHostElement" }, @@ -1220,29 +1136,17 @@ { "name": "setCheckNoChangesMode" }, - { - "name": "setClass" - }, - { - "name": "setClassName" - }, { "name": "setCurrentQueryIndex" }, { - "name": "setCurrentStyleSanitizer" - }, - { - "name": "setDefaultValue" - }, - { - "name": "setDirectiveStylingInput" + "name": "setDirectiveInputsWhichShadowsStyling" }, { "name": "setElementExitFn" }, { - "name": "setGuardMask" + "name": "setHostBindingsByExecutingExpandoInstructions" }, { "name": "setIncludeViewProviders" @@ -1262,12 +1166,6 @@ { "name": "setLContainerActiveIndex" }, - { - "name": "setMapAsDirty" - }, - { - "name": "setMapValue" - }, { "name": "setPreviousOrParentTNode" }, @@ -1275,19 +1173,25 @@ "name": "setSelectedIndex" }, { - "name": "setStyle" + "name": "setTStylingRangeNext" }, { - "name": "setStyleAttr" + "name": "setTStylingRangeNextDuplicate" + }, + { + "name": "setTStylingRangePrev" + }, + { + "name": "setTStylingRangePrevDuplicate" }, { "name": "setUpAttributes" }, { - "name": "setValue" + "name": "shouldSearchParent" }, { - "name": "shouldSearchParent" + "name": "splitClassList" }, { "name": "storeCleanupFn" @@ -1299,16 +1203,10 @@ "name": "stringifyForError" }, { - "name": "stylingApply" + "name": "styleKeyValue" }, { - "name": "stylingMapToString" - }, - { - "name": "stylingProp" - }, - { - "name": "syncContextInitialStyling" + "name": "stylingPropertyFirstUpdatePass" }, { "name": "syncViewWithBlueprint" @@ -1325,6 +1223,12 @@ { "name": "tickRootContext" }, + { + "name": "toTStylingRange" + }, + { + "name": "toggleClass" + }, { "name": "trackByIdentity" }, @@ -1337,21 +1241,6 @@ { "name": "unwrapSafeValue" }, - { - "name": "updateBindingData" - }, - { - "name": "updateClassViaContext" - }, - { - "name": "updateInitialStylingOnContext" - }, - { - "name": "updateRawValueOnContext" - }, - { - "name": "updateStyleViaContext" - }, { "name": "viewAttachedToChangeDetector" }, @@ -1365,7 +1254,16 @@ "name": "wrapListener" }, { - "name": "writeStylingValueDirectly" + "name": "writeAndReconcileClass" + }, + { + "name": "writeAndReconcileStyle" + }, + { + "name": "writeDirectClass" + }, + { + "name": "writeDirectStyle" }, { "name": "ɵɵadvance" diff --git a/packages/core/test/render3/component_ref_spec.ts b/packages/core/test/render3/component_ref_spec.ts index 4ed8043b72..6d5b4375ab 100644 --- a/packages/core/test/render3/component_ref_spec.ts +++ b/packages/core/test/render3/component_ref_spec.ts @@ -8,10 +8,10 @@ import {Injector, NgModuleRef, ViewEncapsulation} from '../../src/core'; import {ComponentFactory} from '../../src/linker/component_factory'; -import {RendererFactory2} from '../../src/render/api'; +import {RendererFactory2, RendererType2} from '../../src/render/api'; import {injectComponentFactoryResolver} from '../../src/render3/component_ref'; -import {ɵɵdefineComponent} from '../../src/render3/index'; -import {domRendererFactory3} from '../../src/render3/interfaces/renderer'; +import {AttributeMarker, ɵɵdefineComponent} from '../../src/render3/index'; +import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {Sanitizer} from '../../src/sanitization/sanitizer'; describe('ComponentFactory', () => { @@ -97,6 +97,7 @@ describe('ComponentFactory', () => { decls: 0, vars: 0, template: () => undefined, + hostAttrs: [AttributeMarker.Classes, 'HOST_COMPONENT'] }); } @@ -291,5 +292,24 @@ describe('ComponentFactory', () => { expect(mSanitizerFactorySpy).toHaveBeenCalled(); }); }); + + it('should ensure that rendererFactory is called after initial styling is set', () => { + const myRendererFactory: RendererFactory3 = { + createRenderer: function(hostElement: RElement|null, rendererType: RendererType2|null): + Renderer3 { + if (hostElement) { + hostElement.classList.add('HOST_RENDERER'); + } + return document; + } + }; + const injector = Injector.create([ + {provide: RendererFactory2, useValue: myRendererFactory}, + ]); + + const hostNode = document.createElement('div'); + const componentRef = cf.create(injector, undefined, hostNode); + expect(hostNode.className).toEqual('HOST_COMPONENT HOST_RENDERER'); + }); }); }); diff --git a/packages/core/test/render3/instructions/lview_debug_spec.ts b/packages/core/test/render3/instructions/lview_debug_spec.ts new file mode 100644 index 0000000000..6b5a3f5800 --- /dev/null +++ b/packages/core/test/render3/instructions/lview_debug_spec.ts @@ -0,0 +1,147 @@ +/** + * @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 {TNodeDebug} from '@angular/core/src/render3/instructions/lview_debug'; +import {createTNode, createTView} from '@angular/core/src/render3/instructions/shared'; +import {TNodeType} from '@angular/core/src/render3/interfaces/node'; +import {LView, TView, TViewType} from '@angular/core/src/render3/interfaces/view'; +import {enterView, leaveView} from '@angular/core/src/render3/state'; +import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list'; + + +describe('lView_debug', () => { + const mockFirstUpdatePassLView: LView = [null, {firstUpdatePass: true}] as any; + beforeEach(() => enterView(mockFirstUpdatePassLView, null)); + afterEach(() => leaveView()); + + describe('TNode', () => { + let tNode !: TNodeDebug; + let tView !: TView; + beforeEach(() => { + tView = createTView(TViewType.Component, 0, null, 0, 0, null, null, null, null, null); + tNode = createTNode(tView, null !, TNodeType.Element, 0, '', null) as TNodeDebug; + }); + afterEach(() => tNode = tView = null !); + + describe('styling', () => { + it('should decode no styling', () => { + expect(tNode.styleBindings_).toEqual([null]); + expect(tNode.classBindings_).toEqual([null]); + }); + + it('should decode static styling', () => { + tNode.styles = 'color: blue'; + tNode.classes = 'STATIC'; + expect(tNode.styleBindings_).toEqual(['color: blue']); + expect(tNode.classBindings_).toEqual(['STATIC']); + }); + + it('should decode no-template property binding', () => { + tNode.classes = 'STATIC'; + insertTStylingBinding(tView.data, tNode, 'CLASS', 2, true, true); + insertTStylingBinding(tView.data, tNode, 'color', 4, true, false); + + expect(tNode.styleBindings_).toEqual([ + null, { + index: 4, + key: 'color', + isTemplate: false, + prevDuplicate: false, + nextDuplicate: false, + prevIndex: 0, + nextIndex: 0, + } + ]); + expect(tNode.classBindings_).toEqual([ + 'STATIC', { + index: 2, + key: 'CLASS', + isTemplate: false, + prevDuplicate: false, + nextDuplicate: false, + prevIndex: 0, + nextIndex: 0, + } + ]); + }); + + it('should decode template and directive property binding', () => { + tNode.classes = 'STATIC'; + insertTStylingBinding(tView.data, tNode, 'CLASS', 2, false, true); + insertTStylingBinding(tView.data, tNode, 'color', 4, false, false); + + expect(tNode.styleBindings_).toEqual([ + null, { + index: 4, + key: 'color', + isTemplate: true, + prevDuplicate: false, + nextDuplicate: false, + prevIndex: 0, + nextIndex: 0, + } + ]); + expect(tNode.classBindings_).toEqual([ + 'STATIC', { + index: 2, + key: 'CLASS', + isTemplate: true, + prevDuplicate: false, + nextDuplicate: false, + prevIndex: 0, + nextIndex: 0, + } + ]); + + insertTStylingBinding(tView.data, tNode, STYLE_MAP_STYLING_KEY, 6, true, true); + insertTStylingBinding(tView.data, tNode, CLASS_MAP_STYLING_KEY, 8, true, false); + + expect(tNode.styleBindings_).toEqual([ + null, { + index: 8, + key: CLASS_MAP_STYLING_KEY, + isTemplate: false, + prevDuplicate: false, + nextDuplicate: true, + prevIndex: 0, + nextIndex: 4, + }, + { + index: 4, + key: 'color', + isTemplate: true, + prevDuplicate: true, + nextDuplicate: false, + prevIndex: 8, + nextIndex: 0, + } + ]); + expect(tNode.classBindings_).toEqual([ + 'STATIC', { + index: 6, + key: STYLE_MAP_STYLING_KEY, + isTemplate: false, + prevDuplicate: true, + nextDuplicate: true, + prevIndex: 0, + nextIndex: 2, + }, + { + index: 2, + key: 'CLASS', + isTemplate: true, + prevDuplicate: true, + nextDuplicate: false, + prevIndex: 6, + nextIndex: 0, + } + ]); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index be6f6b5fa2..09139881ad 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -7,11 +7,12 @@ */ import {NgForOfContext} from '@angular/common'; +import {getSortedClassName} from '@angular/core/testing/src/styling'; import {ɵɵdefineComponent} from '../../src/render3/definition'; import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index'; import {AttributeMarker} from '../../src/render3/interfaces/node'; -import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass'; +import {SafeValue, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass'; import {ɵɵdefaultStyleSanitizer, ɵɵsanitizeHtml, ɵɵsanitizeResourceUrl, ɵɵsanitizeScript, ɵɵsanitizeStyle, ɵɵsanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer} from '../../src/sanitization/sanitizer'; import {SecurityContext} from '../../src/sanitization/security'; @@ -137,18 +138,20 @@ describe('instructions', () => { describe('styleProp', () => { it('should automatically sanitize unless a bypass operation is applied', () => { - const t = new TemplateFixture(() => { return createDiv(); }, () => {}, 1); - t.update(() => { - ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); - ɵɵstyleProp('background-image', 'url("http://server")'); - }); + let backgroundImage: string|SafeValue = 'url("http://server")'; + const t = new TemplateFixture( + () => { return createDiv(); }, + () => { + ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); + ɵɵstyleProp('background-image', backgroundImage); + }, + 2, 2); // nothing is set because sanitizer suppresses it. - expect(t.html).toEqual('
'); + expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) + .toEqual(''); - t.update(() => { - ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); - ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('url("http://server2")')); - }); + backgroundImage = bypassSanitizationTrustStyle('url("http://server2")'); + t.update(); expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) .toEqual('url("http://server2")'); }); @@ -160,9 +163,10 @@ describe('instructions', () => { function createDivWithStyle() { ɵɵelement(0, 'div', 0); } it('should add style', () => { - const fixture = new TemplateFixture( - createDivWithStyle, () => {}, 1, 0, null, null, null, undefined, attrs); - fixture.update(() => { ɵɵstyleMap({'background-color': 'red'}); }); + const fixture = new TemplateFixture(createDivWithStyle, () => { + ɵɵstyleMap({'background-color': 'red'}); + }, 1, 2, null, null, null, undefined, attrs); + fixture.update(); expect(fixture.html).toEqual('
'); }); @@ -184,7 +188,7 @@ describe('instructions', () => { 'width': 'width' }); }, - 1, 0, null, null, sanitizerInterceptor); + 1, 2, null, null, sanitizerInterceptor); const props = detectedValues.sort(); expect(props).toEqual([ @@ -197,9 +201,10 @@ describe('instructions', () => { function createDivWithStyling() { ɵɵelement(0, 'div'); } it('should add class', () => { - const fixture = - new TemplateFixture(createDivWithStyling, () => { ɵɵclassMap('multiple classes'); }, 1); - expect(fixture.html).toEqual('
'); + const fixture = new TemplateFixture( + createDivWithStyling, () => { ɵɵclassMap('multiple classes'); }, 1, 2); + const div = fixture.containerElement.querySelector('div.multiple') !; + expect(getSortedClassName(div)).toEqual('classes multiple'); }); }); diff --git a/packages/core/test/render3/styling_next/class_differ_spec.ts b/packages/core/test/render3/styling_next/class_differ_spec.ts index 7f91bfabcd..3c377803e5 100644 --- a/packages/core/test/render3/styling_next/class_differ_spec.ts +++ b/packages/core/test/render3/styling_next/class_differ_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {classIndexOf, computeClassChanges, removeClass, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ'; +import {classIndexOf, computeClassChanges, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ'; describe('class differ', () => { describe('computeClassChanges', () => { @@ -81,25 +81,25 @@ describe('class differ', () => { }); }); - describe('removeClass', () => { + describe('toggleClass', () => { it('should remove class name from a class-list string', () => { - expect(removeClass('', '')).toEqual(''); - expect(removeClass('A', 'A')).toEqual(''); - expect(removeClass('AB', 'AB')).toEqual(''); - expect(removeClass('A B', 'A')).toEqual('B'); - expect(removeClass('A B', 'A')).toEqual('B'); + expect(toggleClass('', '', false)).toEqual(''); + expect(toggleClass('A', 'A', false)).toEqual(''); + expect(toggleClass('AB', 'AB', false)).toEqual(''); + expect(toggleClass('A B', 'A', false)).toEqual('B'); + expect(toggleClass('A B', 'A', false)).toEqual('B'); + expect(toggleClass('A B', 'B', false)).toEqual('A'); + expect(toggleClass(' B ', 'B', false)).toEqual(''); }); it('should not remove a sub-string', () => { - expect(removeClass('ABC', 'A')).toEqual('ABC'); - expect(removeClass('ABC', 'B')).toEqual('ABC'); - expect(removeClass('ABC', 'C')).toEqual('ABC'); - expect(removeClass('ABC', 'AB')).toEqual('ABC'); - expect(removeClass('ABC', 'BC')).toEqual('ABC'); + expect(toggleClass('ABC', 'A', false)).toEqual('ABC'); + expect(toggleClass('ABC', 'B', false)).toEqual('ABC'); + expect(toggleClass('ABC', 'C', false)).toEqual('ABC'); + expect(toggleClass('ABC', 'AB', false)).toEqual('ABC'); + expect(toggleClass('ABC', 'BC', false)).toEqual('ABC'); }); - }); - describe('removeClass', () => { it('should toggle a class', () => { expect(toggleClass('', 'B', false)).toEqual(''); expect(toggleClass('', 'B', true)).toEqual('B'); diff --git a/packages/core/test/render3/styling_next/reconcile_spec.ts b/packages/core/test/render3/styling_next/reconcile_spec.ts index 2ae9160b21..8ee940a6c4 100644 --- a/packages/core/test/render3/styling_next/reconcile_spec.ts +++ b/packages/core/test/render3/styling_next/reconcile_spec.ts @@ -8,6 +8,7 @@ import {Renderer3, domRendererFactory3} from '@angular/core/src/render3/interfaces/renderer'; import {writeAndReconcileClass, writeAndReconcileStyle} from '@angular/core/src/render3/styling/reconcile'; +import {getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling'; describe('styling reconcile', () => { [document, domRendererFactory3.createRenderer(null, null)].forEach((renderer: Renderer3) => { @@ -84,41 +85,3 @@ describe('styling reconcile', () => { }); }); }); - -function getSortedClassName(element: HTMLElement): string { - const names: string[] = []; - const classList = element.classList || []; - for (let i = 0; i < classList.length; i++) { - const name = classList[i]; - if (names.indexOf(name) === -1) { - names.push(name); - } - } - names.sort(); - return names.join(' '); -} - -function getSortedStyle(element: HTMLElement): string { - const names: string[] = []; - const style = element.style; - // reading `style.color` is a work around for a bug in Domino. The issue is that Domino has stale - // value for `style.length`. It seems that reading a property from the element causes the stale - // value to be updated. (As of Domino v 2.1.3) - style.color; - for (let i = 0; i < style.length; i++) { - const name = style.item(i); - if (names.indexOf(name) === -1) { - names.push(name); - } - } - names.sort(); - let sorted = ''; - names.forEach(key => { - const value = style.getPropertyValue(key); - if (value != null && value !== '') { - if (sorted !== '') sorted += ' '; - sorted += key + ': ' + value + ';'; - } - }); - return sorted; -} \ No newline at end of file diff --git a/packages/core/test/render3/styling_next/style_binding_list_spec.ts b/packages/core/test/render3/styling_next/style_binding_list_spec.ts index 260427c060..45fe5238c3 100644 --- a/packages/core/test/render3/styling_next/style_binding_list_spec.ts +++ b/packages/core/test/render3/styling_next/style_binding_list_spec.ts @@ -12,7 +12,6 @@ import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDu import {LView, TData} from '@angular/core/src/render3/interfaces/view'; import {enterView, leaveView} from '@angular/core/src/render3/state'; import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, appendStyling, flushStyleBinding, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list'; -import {getStylingBindingHead} from '@angular/core/src/render3/styling/styling_debug'; import {newArray} from '@angular/core/src/util/array_utils'; describe('TNode styling linked list', () => { @@ -438,34 +437,47 @@ describe('TNode styling linked list', () => { it('should write basic value', () => { const fixture = new StylingFixture([['color']], false); fixture.setBinding(0, 'red'); - expect(fixture.flush(0)).toEqual('color: red'); + expect(fixture.flush(0)).toEqual('color: red;'); }); it('should chain values and allow update mid list', () => { const fixture = new StylingFixture([['color', {key: 'width', extra: 'px'}]], false); fixture.setBinding(0, 'red'); fixture.setBinding(1, '100'); - expect(fixture.flush(0)).toEqual('color: red; width: 100px'); + expect(fixture.flush(0)).toEqual('color: red; width: 100px;'); fixture.setBinding(0, 'blue'); fixture.setBinding(1, '200'); - expect(fixture.flush(1)).toEqual('color: red; width: 200px'); - expect(fixture.flush(0)).toEqual('color: blue; width: 200px'); + expect(fixture.flush(1)).toEqual('color: red; width: 200px;'); + expect(fixture.flush(0)).toEqual('color: blue; width: 200px;'); }); it('should remove duplicates', () => { const fixture = new StylingFixture([['color', 'color']], false); fixture.setBinding(0, 'red'); fixture.setBinding(1, 'blue'); - expect(fixture.flush(0)).toEqual('color: blue'); + expect(fixture.flush(0)).toEqual('color: blue;'); + }); + + it('should treat undefined values as previous value', () => { + const fixture = new StylingFixture([['color', 'color']], false); + fixture.setBinding(0, 'red'); + fixture.setBinding(1, undefined); + expect(fixture.flush(0)).toEqual('color: red;'); + }); + + it('should treat null value as removal', () => { + const fixture = new StylingFixture([['color']], false); + fixture.setBinding(0, null); + expect(fixture.flush(0)).toEqual(''); }); }); describe('appendStyling', () => { it('should append simple style', () => { - expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red'); - expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red'); + expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red;'); + expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red;'); expect(appendStyling('', 'color', 'red', null, false, true)).toEqual('color'); expect(appendStyling('', 'color', 'red', null, true, true)).toEqual('color'); expect(appendStyling('', 'color', true, null, true, true)).toEqual('color'); @@ -476,25 +488,25 @@ describe('TNode styling linked list', () => { it('should append simple style with suffix', () => { expect(appendStyling('', {key: 'width', extra: 'px'}, 100, null, false, false)) - .toEqual('width: 100px'); + .toEqual('width: 100px;'); }); it('should append simple style with sanitizer', () => { expect( appendStyling('', {key: 'width', extra: (v: any) => `-${v}-`}, 100, null, false, false)) - .toEqual('width: -100-'); + .toEqual('width: -100-;'); }); it('should append class/style', () => { - expect(appendStyling('color: white', 'color', 'red', null, false, false)) - .toEqual('color: white; color: red'); + expect(appendStyling('color: white;', 'color', 'red', null, false, false)) + .toEqual('color: white; color: red;'); expect(appendStyling('MY-CLASS', 'color', true, null, false, true)).toEqual('MY-CLASS color'); expect(appendStyling('MY-CLASS', 'color', false, null, true, true)).toEqual('MY-CLASS'); }); it('should remove existing', () => { - expect(appendStyling('color: white', 'color', 'blue', null, true, false)) - .toEqual('color: blue'); + expect(appendStyling('color: white;', 'color', 'blue', null, true, false)) + .toEqual('color: blue;'); expect(appendStyling('A YES B', 'YES', false, null, true, true)).toEqual('A B'); }); @@ -510,10 +522,10 @@ describe('TNode styling linked list', () => { it('should support maps for styles', () => { expect(appendStyling('', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false)) - .toEqual('A: a; B: b'); + .toEqual('A: a; B: b;'); expect(appendStyling( - 'A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false)) - .toEqual('C:_; A: a; B: b'); + 'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false)) + .toEqual('C:_; A: a; B: b;'); }); it('should support strings for classes', () => { @@ -525,11 +537,11 @@ describe('TNode styling linked list', () => { }); it('should support strings for styles', () => { - expect(appendStyling('A:a;B:b', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false)) - .toEqual('A:a;B:b; A : a ; B : b'); - expect( - appendStyling('A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false)) - .toEqual('C:_; A: a; B: b'); + expect(appendStyling('A:a;B:b;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false)) + .toEqual('A:a;B:b; A : a ; B : b;'); + expect(appendStyling( + 'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false)) + .toEqual('C:_; A: a; B: b;'); }); it('should throw no arrays for styles', () => { @@ -560,7 +572,7 @@ describe('TNode styling linked list', () => { 'list-style: unsafe; ' + 'list-style-image: unsafe; ' + 'clip-path: unsafe; ' + - 'width: url(javascript:evil())'); + 'width: url(javascript:evil());'); // verify string expect(appendStyling( '', STYLE_MAP_STYLING_KEY, @@ -571,7 +583,7 @@ describe('TNode styling linked list', () => { 'list-style: url(javascript:evil());' + 'list-style-image: url(javascript:evil());' + 'clip-path: url(javascript:evil());' + - 'width: url(javascript:evil())' // should not sanitize + 'width: url(javascript:evil());' // should not sanitize , null, true, false)) .toEqual( @@ -582,7 +594,7 @@ describe('TNode styling linked list', () => { 'list-style: unsafe; ' + 'list-style-image: unsafe; ' + 'clip-path: unsafe; ' + - 'width: url(javascript:evil())'); + 'width: url(javascript:evil());'); }); }); }); @@ -632,6 +644,24 @@ function expectPriorityOrder(tData: TData, tNode: TNode, isClassBinding: boolean return expect(indexes); } + +/** + * Find the head of the styling binding linked list. + */ +export function getStylingBindingHead(tData: TData, tNode: TNode, isClassBinding: boolean): number { + let index = getTStylingRangePrev(isClassBinding ? tNode.classBindings : tNode.styleBindings); + while (true) { + const tStylingRange = tData[index + 1] as TStylingRange; + const prev = getTStylingRangePrev(tStylingRange); + if (prev === 0) { + // found head exit. + return index; + } else { + index = prev; + } + } +} + class StylingFixture { tData: TData = [null, null]; lView: LView = [null, null !] as any; diff --git a/packages/core/test/render3/styling_next/style_differ_spec.ts b/packages/core/test/render3/styling_next/style_differ_spec.ts index 6d7d51e995..b8ce8f0f33 100644 --- a/packages/core/test/render3/styling_next/style_differ_spec.ts +++ b/packages/core/test/render3/styling_next/style_differ_spec.ts @@ -7,8 +7,7 @@ */ import {StyleChangesMap, parseKeyValue, removeStyle} from '@angular/core/src/render3/styling/style_differ'; -import {consumeSeparatorWithWhitespace, consumeStyleValue} from '@angular/core/src/render3/styling/styling_parser'; -import {CharCode} from '@angular/core/src/util/char_code'; +import {getLastParsedValue, parseStyle} from '@angular/core/src/render3/styling/styling_parser'; import {sortedForEach} from './class_differ_spec'; describe('style differ', () => { @@ -31,6 +30,13 @@ describe('style differ', () => { expectParseValue(': text1 text2 ;🛑').toBe('text1 text2'); }); + it('should parse empty vale', () => { + expectParseValue(':').toBe(''); + expectParseValue(': ').toBe(''); + expectParseValue(': ;🛑').toBe(''); + expectParseValue(':;🛑').toBe(''); + }); + it('should parse quoted values', () => { expectParseValue(':""').toBe('""'); expectParseValue(':"\\\\"').toBe('"\\\\"'); @@ -54,11 +60,16 @@ describe('style differ', () => { }); describe('parseKeyValue', () => { - it('should parse empty value', () => { + it('should parse empty string', () => { expectParseKeyValue('').toEqual([]); expectParseKeyValue(' \n\t\r ').toEqual([]); }); + it('should parse empty value', () => { + expectParseKeyValue('key:').toEqual(['key', '', null]); + expectParseKeyValue('key: \n\t\r; ').toEqual(['key', '', null]); + }); + it('should prase single style', () => { expectParseKeyValue('width: 100px').toEqual(['width', '100px', null]); expectParseKeyValue(' width : 100px ;').toEqual(['width', '100px', null]); @@ -79,27 +90,27 @@ describe('style differ', () => { describe('removeStyle', () => { it('should remove no style', () => { expect(removeStyle('', 'foo')).toEqual(''); - expect(removeStyle('abc: bar', 'a')).toEqual('abc: bar'); - expect(removeStyle('abc: bar', 'b')).toEqual('abc: bar'); - expect(removeStyle('abc: bar', 'c')).toEqual('abc: bar'); - expect(removeStyle('abc: bar', 'bar')).toEqual('abc: bar'); + expect(removeStyle('abc: bar;', 'a')).toEqual('abc: bar;'); + expect(removeStyle('abc: bar;', 'b')).toEqual('abc: bar;'); + expect(removeStyle('abc: bar;', 'c')).toEqual('abc: bar;'); + expect(removeStyle('abc: bar;', 'bar')).toEqual('abc: bar;'); }); it('should remove all style', () => { - expect(removeStyle('foo: bar', 'foo')).toEqual(''); + expect(removeStyle('foo: bar;', 'foo')).toEqual(''); expect(removeStyle('foo: bar; foo: bar;', 'foo')).toEqual(''); }); it('should remove some of the style', () => { - expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b'); - expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b'); - expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c', 'foo')) - .toEqual('a: a; b: b; c: c'); + expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;'); + expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;'); + expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c;', 'foo')) + .toEqual('a: a; b: b; c: c;'); }); it('should remove trailing ;', () => { - expect(removeStyle('a: a; foo: bar', 'foo')).toEqual('a: a'); - expect(removeStyle('a: a ; foo: bar ; ', 'foo')).toEqual('a: a'); + expect(removeStyle('a: a; foo: bar;', 'foo')).toEqual('a: a;'); + expect(removeStyle('a: a ; foo: bar ; ', 'foo')).toEqual('a: a ;'); }); }); }); @@ -114,11 +125,9 @@ function expectParseValue( text: string) { let stopIndex = text.indexOf('🛑'); if (stopIndex < 0) stopIndex = text.length; - const valueStart = consumeSeparatorWithWhitespace(text, 0, text.length, CharCode.COLON); - const valueEnd = consumeStyleValue(text, valueStart, text.length); - const valueSep = consumeSeparatorWithWhitespace(text, valueEnd, text.length, CharCode.SEMI_COLON); - expect(valueSep).toBe(stopIndex); - return expect(text.substring(valueStart, valueEnd)); + let i = parseStyle(text); + expect(i).toBe(stopIndex); + return expect(getLastParsedValue(text)); } function expectParseKeyValue(text: string) { diff --git a/packages/core/testing/src/styling.ts b/packages/core/testing/src/styling.ts new file mode 100644 index 0000000000..dcb4ba904f --- /dev/null +++ b/packages/core/testing/src/styling.ts @@ -0,0 +1,84 @@ +/** + * @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 + */ + +/** + * Returns element classes in form of a stable (sorted) string. + * + * @param element HTML Element. + * @returns Returns element classes in form of a stable (sorted) string. + */ +export function getSortedClassName(element: Element): string { + const names: string[] = Object.keys(getElementClasses(element)); + names.sort(); + return names.join(' '); +} + +/** + * Returns element classes in form of a map. + * + * @param element HTML Element. + * @returns Map of class values. + */ +export function getElementClasses(element: Element): {[key: string]: true} { + const classes: {[key: string]: true} = {}; + if (element.nodeType === Node.ELEMENT_NODE) { + const classList = element.classList; + for (let i = 0; i < classList.length; i++) { + const key = classList[i]; + classes[key] = true; + } + } + return classes; +} + +/** + * Returns element styles in form of a stable (sorted) string. + * + * @param element HTML Element. + * @returns Returns element styles in form of a stable (sorted) string. + */ +export function getSortedStyle(element: Element): string { + const styles = getElementStyles(element); + const names: string[] = Object.keys(styles); + names.sort(); + let sorted = ''; + names.forEach(key => { + const value = styles[key]; + if (value != null && value !== '') { + if (sorted !== '') sorted += ' '; + sorted += key + ': ' + value + ';'; + } + }); + return sorted; +} + +/** + * Returns element styles in form of a map. + * + * @param element HTML Element. + * @returns Map of style values. + */ +export function getElementStyles(element: Element): {[key: string]: string} { + const styles: {[key: string]: string} = {}; + if (element.nodeType === Node.ELEMENT_NODE) { + const style = (element as HTMLElement).style; + // reading `style.color` is a work around for a bug in Domino. The issue is that Domino has + // stale value for `style.length`. It seems that reading a property from the element causes the + // stale value to be updated. (As of Domino v 2.1.3) + style.color; + for (let i = 0; i < style.length; i++) { + const key = style.item(i); + const value = style.getPropertyValue(key); + if (value !== '') { + // Workaround for IE not clearing properties, instead it just sets them to blank value. + styles[key] = value; + } + } + } + return styles; +} \ No newline at end of file diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index 14235e9cc7..4f25cd39d1 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -195,7 +195,10 @@ export declare class NgClass implements DoCheck { [klass: string]: any; }); constructor(_iterableDiffers: IterableDiffers, _keyValueDiffers: KeyValueDiffers, _ngEl: ElementRef, _renderer: Renderer2); + applyChanges(): void; ngDoCheck(): void; + setClass(value: string): void; + setNgClass(value: any): void; } export declare class NgComponentOutlet implements OnChanges, OnDestroy { @@ -270,7 +273,9 @@ export declare class NgStyle implements DoCheck { [klass: string]: any; } | null); constructor(_ngEl: ElementRef, _differs: KeyValueDiffers, _renderer: Renderer2); + applyChanges(): void; ngDoCheck(): void; + setNgStyle(value: any): void; } export declare class NgSwitch { diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 6516edcf3a..b9ec3e0da5 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -703,8 +703,8 @@ export declare function ɵɵattributeInterpolate8(attrName: string, prefix: stri export declare function ɵɵattributeInterpolateV(attrName: string, values: any[], sanitizer?: SanitizerFn, namespace?: string): typeof ɵɵattributeInterpolateV; export declare function ɵɵclassMap(classes: { - [className: string]: any; -} | NO_CHANGE | string | null): void; + [className: string]: boolean | null | undefined; +} | Map | Set | string[] | string | null | undefined): void; export declare function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void; @@ -724,7 +724,7 @@ export declare function ɵɵclassMapInterpolate8(prefix: string, v0: any, i0: st export declare function ɵɵclassMapInterpolateV(values: any[]): void; -export declare function ɵɵclassProp(className: string, value: boolean | null): typeof ɵɵclassProp; +export declare function ɵɵclassProp(className: string, value: boolean | null | undefined): typeof ɵɵclassProp; export declare type ɵɵComponentDefWithMeta(predicate: Type | string[], export declare function ɵɵstyleMap(styles: { [styleName: string]: any; -} | NO_CHANGE | null): void; +} | Map | string | null | undefined): void; -export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | null, suffix?: string | null): typeof ɵɵstyleProp; +export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | null | undefined, suffix?: string | null): typeof ɵɵstyleProp; export declare function ɵɵstylePropInterpolate1(prop: string, prefix: string, v0: any, suffix: string, valueSuffix?: string | null): typeof ɵɵstylePropInterpolate1;