diff --git a/modules/benchmarks/src/largetable/render3/table.ts b/modules/benchmarks/src/largetable/render3/table.ts index ba70e62002..5f4a94caa1 100644 --- a/modules/benchmarks/src/largetable/render3/table.ts +++ b/modules/benchmarks/src/largetable/render3/table.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵC as C, ɵE as E, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as detectChanges, ɵe as e, ɵs as s, ɵt as t, ɵv as v} from '@angular/core'; +import {ɵC as C, ɵE as E, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as detectChanges, ɵe as e, ɵsn as sn, ɵt as t, ɵv as v} from '@angular/core'; import {ComponentDef} from '@angular/core/src/render3/interfaces/definition'; import {TableCell, buildTable, emptyTable} from '../util'; @@ -48,7 +48,7 @@ export class LargeTableComponent { { T(1); } e(); } - s(0, 'background-color', b(cell.row % 2 ? '' : 'grey')); + sn(0, 'background-color', b(cell.row % 2 ? '' : 'grey')); t(1, b(cell.value)); } v(); diff --git a/modules/benchmarks/src/tree/render3/tree.ts b/modules/benchmarks/src/tree/render3/tree.ts index 61f2ad41ce..fb7ae81597 100644 --- a/modules/benchmarks/src/tree/render3/tree.ts +++ b/modules/benchmarks/src/tree/render3/tree.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵC as C, ɵE as E, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as _detectChanges, ɵe as e, ɵi1 as i1, ɵp as p, ɵs as s, ɵt as t, ɵv as v} from '@angular/core'; +import {ɵC as C, ɵE as E, ɵT as T, ɵV as V, ɵb as b, ɵcR as cR, ɵcr as cr, ɵdefineComponent as defineComponent, ɵdetectChanges as _detectChanges, ɵe as e, ɵi1 as i1, ɵp as p, ɵsn as sn, ɵt as t, ɵv as v} from '@angular/core'; import {ComponentDef} from '@angular/core/src/render3/interfaces/definition'; import {TreeNode, buildTree, emptyTree} from '../util'; @@ -46,7 +46,7 @@ export class TreeComponent { C(2); C(3); } - s(0, 'background-color', b(ctx.data.depth % 2 ? '' : 'grey')); + sn(0, 'background-color', b(ctx.data.depth % 2 ? '' : 'grey')); t(1, i1(' ', ctx.data.value, ' ')); cR(2); { @@ -114,7 +114,7 @@ export function TreeTpl(ctx: TreeNode, cm: boolean) { } e(); } - s(1, 'background-color', b(ctx.depth % 2 ? '' : 'grey')); + sn(1, 'background-color', b(ctx.depth % 2 ? '' : 'grey')); t(2, i1(' ', ctx.value, ' ')); cR(3); { diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 6705744811..eb6fd3b52c 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -63,7 +63,9 @@ export { p as ɵp, pD as ɵpD, a as ɵa, + s as ɵs, sn as ɵsn, + k as ɵk, kn as ɵkn, t as ɵt, v as ɵv, diff --git a/packages/core/src/render3/PERF_NOTES.md b/packages/core/src/render3/PERF_NOTES.md index c373b665b8..ce9c4f0c1e 100644 --- a/packages/core/src/render3/PERF_NOTES.md +++ b/packages/core/src/render3/PERF_NOTES.md @@ -50,4 +50,15 @@ export function getExported() { return exported; } export function setExported(v) { exported = v; } ``` -Also writing to a property of `exports` might change its hidden class resulting in megamorphic access. \ No newline at end of file +Also writing to a property of `exports` might change its hidden class resulting in megamorphic access. + +## Iterating over Keys of an Object. + +https://jsperf.com/object-keys-vs-for-in-with-closure/3 implies that `Object.keys` is the fastest way of iterating +over properties of an object. + +``` +for (var i = 0, keys = Object.keys(obj); i < keys.length; i++) { + const key = keys[i]; +} +``` \ No newline at end of file diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index f63c418eba..fda3625c65 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -45,10 +45,12 @@ export { containerRefreshEnd as cr, elementAttribute as a, + elementClass as k, elementClassNamed as kn, elementEnd as e, elementProperty as p, elementStart as E, + elementStyle as s, elementStyleNamed as sn, listener as L, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index aa3ce2613f..35f9de3493 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -866,7 +866,7 @@ function generatePropertyAliases(lNodeFlags: number, direction: BindingDirection } /** - * Add or remove a class in a classList. + * Add or remove a class in a `classList` on a DOM element. * * This instruction is meant to handle the [class.foo]="exp" case * @@ -889,6 +889,29 @@ export function elementClassNamed(index: number, className: string, value: T } } +/** + * Set the `className` property on a DOM element. + * + * This instruction is meant to handle the `[class]="exp"` usage. + * + * `elementClass` instruction writes the value to the "element's" `className` property. + * + * @param index The index of the element to update in the data array + * @param value A value indicating a set of classes which should be applied. The method overrides + * any existing classes. The value is stringified (`toString`) before it is applied to the + * element. + */ +export function elementClass(index: number, value: T | NO_CHANGE): void { + if (value !== NO_CHANGE) { + // TODO: This is a naive implementation which simply writes value to the `className`. In the + // future + // we will add logic here which would work with the animation code. + const lElement: LElementNode = data[index]; + isProceduralRenderer(renderer) ? renderer.setProperty(lElement.native, 'className', value) : + lElement.native['className'] = stringify(value); + } +} + /** * Update a given style on an Element. * @@ -908,22 +931,56 @@ export function elementStyleNamed( index: number, styleName: string, value: T | NO_CHANGE, suffixOrSanitizer?: string | Sanitizer): void { if (value !== NO_CHANGE) { - const lElement = data[index] as LElementNode; + const lElement: LElementNode = data[index]; if (value == null) { isProceduralRenderer(renderer) ? renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) : - lElement.native.style.removeProperty(styleName); + lElement.native['style'].removeProperty(styleName); } else { let strValue = typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value); if (typeof suffixOrSanitizer == 'string') strValue = strValue + suffixOrSanitizer; isProceduralRenderer(renderer) ? renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) : - lElement.native.style.setProperty(styleName, strValue); + lElement.native['style'].setProperty(styleName, strValue); } } } +/** + * Set the `style` property on a DOM element. + * + * This instruction is meant to handle the `[style]="exp"` usage. + * + * + * @param index The index of the element to update in the data array + * @param value A value indicating if a given style should be added or removed. + * The expected shape of `value` is an object where keys are style names and the values + * are their corresponding values to set. If value is falsy than the style is remove. An absence + * of style does not cause that style to be removed. `NO_CHANGE` implies that no update should be + * performed. + */ +export function elementStyle( + index: number, value: {[styleName: string]: any} | NO_CHANGE): void { + if (value !== NO_CHANGE) { + // TODO: This is a naive implementation which simply writes value to the `style`. In the future + // we will add logic here which would work with the animation code. + const lElement = data[index] as LElementNode; + if (isProceduralRenderer(renderer)) { + renderer.setProperty(lElement.native, 'style', value); + } else { + const style = lElement.native['style']; + for (let i = 0, keys = Object.keys(value); i < keys.length; i++) { + const styleName: string = keys[i]; + const styleValue: any = (value as any)[styleName]; + styleValue == null ? style.removeProperty(styleName) : + style.setProperty(styleName, styleValue); + } + } + } +} + + ////////////////////////// //// Text diff --git a/packages/core/src/render3/interfaces/renderer.ts b/packages/core/src/render3/interfaces/renderer.ts index 042e6f996f..b9bd50cf8c 100644 --- a/packages/core/src/render3/interfaces/renderer.ts +++ b/packages/core/src/render3/interfaces/renderer.ts @@ -121,6 +121,7 @@ export interface RNode { export interface RElement extends RNode { style: RCssStyleDeclaration; classList: RDomTokenList; + className: string; setAttribute(name: string, value: string): void; removeAttribute(name: string): void; setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void; diff --git a/packages/core/test/render3/compiler_canonical/elements_spec.ts b/packages/core/test/render3/compiler_canonical/elements_spec.ts index 1700f7bf83..1a8d66246e 100644 --- a/packages/core/test/render3/compiler_canonical/elements_spec.ts +++ b/packages/core/test/render3/compiler_canonical/elements_spec.ts @@ -9,7 +9,7 @@ import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core'; import * as $r3$ from '../../../src/core_render3_private_export'; -import {renderComponent, toHtml} from '../render_util'; +import {ComponentFixture, renderComponent, toHtml} from '../render_util'; /// See: `normative.md` describe('elements', () => { @@ -264,5 +264,36 @@ describe('elements', () => { $r3$.ɵdetectChanges(comp); expect(toHtml(comp)).toEqual('
'); }); + + it('should bind [class] and [style] to the element', () => { + type $StyleComponent$ = StyleComponent; + + @Component( + {selector: 'style-comp', template: `
`}) + class StyleComponent { + classExp: string[]|string = 'some-name'; + styleExp: {[name: string]: string} = {'background-color': 'red'}; + + // NORMATIVE + static ngComponentDef = $r3$.ɵdefineComponent({ + type: StyleComponent, + tag: 'style-comp', + factory: function StyleComponent_Factory() { return new StyleComponent(); }, + template: function StyleComponent_Template(ctx: $StyleComponent$, cm: $boolean$) { + if (cm) { + $r3$.ɵE(0, 'div'); + $r3$.ɵe(); + } + $r3$.ɵk(0, $r3$.ɵb(ctx.classExp)); + $r3$.ɵs(0, $r3$.ɵb(ctx.styleExp)); + } + }); + // /NORMATIVE + } + + const styleFixture = new ComponentFixture(StyleComponent); + expect(styleFixture.html) + .toEqual(`
`); + }); }); }); diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index 392a4ef9a9..c654954a7f 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {elementAttribute, elementEnd, elementProperty, elementStart, elementStyleNamed, renderTemplate} from '../../src/render3/instructions'; +import {elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, renderTemplate} from '../../src/render3/instructions'; import {LElementNode, LNode} from '../../src/render3/interfaces/node'; import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; @@ -58,7 +58,8 @@ describe('instructions', () => { describe('elementStyleNamed', () => { it('should use sanitizer function', () => { const t = new TemplateFixture(createDiv); - t.update(() => elementStyleNamed(0, 'background-image', 'url("http://server")', sanitizeStyle)); + t.update( + () => elementStyleNamed(0, 'background-image', 'url("http://server")', sanitizeStyle)); // nothing is set because sanitizer suppresses it. expect(t.html).toEqual('
'); @@ -70,4 +71,26 @@ describe('instructions', () => { .toEqual('url("http://server")'); }); }); + + describe('elementStyle', () => { + function createDivWithStyle() { + elementStart(0, 'div', ['style', 'height: 10px']); + elementEnd(); + } + const fixture = new TemplateFixture(createDivWithStyle); + + it('should add style', () => { + fixture.update(() => elementStyle(0, {'background-color': 'red'})); + expect(fixture.html).toEqual('
'); + }); + }); + + describe('elementClass', () => { + const fixture = new TemplateFixture(createDiv); + + it('should add class', () => { + fixture.update(() => elementClass(0, 'multiple classes')); + expect(fixture.html).toEqual('
'); + }); + }); });