test(ivy): add canonical compiler spec for class/style (#22719)

Adds a stub for `elementStyle` and `elementClass` instruction
with a canonical spec for the compiler. The spec shows the the
compiler should be using `elementStyle` and `elementClass` instruction
in place of `[class]` and `[style]` bindings respectively.
PR Close #22719
This commit is contained in:
Miško Hevery 2018-03-08 13:57:56 -08:00 committed by Kara Erickson
parent a0a01f1e1e
commit 112431db69
9 changed files with 140 additions and 13 deletions

View File

@ -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();

View File

@ -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);
{

View File

@ -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,

View File

@ -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.
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];
}
```

View File

@ -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,

View File

@ -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<T>(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<T>(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<T>(
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<T>(
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

View File

@ -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;

View File

@ -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('<div class="" id="changed1" style="color: red;"></div>');
});
it('should bind [class] and [style] to the element', () => {
type $StyleComponent$ = StyleComponent;
@Component(
{selector: 'style-comp', template: `<div [class]="classExp" [style]="styleExp"></div>`})
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(`<div class="some-name" style="background-color: red;"></div>`);
});
});
});

View File

@ -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('<div></div>');
@ -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('<div style="height: 10px; background-color: red;"></div>');
});
});
describe('elementClass', () => {
const fixture = new TemplateFixture(createDiv);
it('should add class', () => {
fixture.update(() => elementClass(0, 'multiple classes'));
expect(fixture.html).toEqual('<div class="multiple classes"></div>');
});
});
});