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:
parent
a0a01f1e1e
commit
112431db69
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {ComponentDef} from '@angular/core/src/render3/interfaces/definition';
|
||||||
|
|
||||||
import {TableCell, buildTable, emptyTable} from '../util';
|
import {TableCell, buildTable, emptyTable} from '../util';
|
||||||
|
@ -48,7 +48,7 @@ export class LargeTableComponent {
|
||||||
{ T(1); }
|
{ T(1); }
|
||||||
e();
|
e();
|
||||||
}
|
}
|
||||||
s(0, 'background-color', b(cell.row % 2 ? '' : 'grey'));
|
sn(0, 'background-color', b(cell.row % 2 ? '' : 'grey'));
|
||||||
t(1, b(cell.value));
|
t(1, b(cell.value));
|
||||||
}
|
}
|
||||||
v();
|
v();
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {ComponentDef} from '@angular/core/src/render3/interfaces/definition';
|
||||||
|
|
||||||
import {TreeNode, buildTree, emptyTree} from '../util';
|
import {TreeNode, buildTree, emptyTree} from '../util';
|
||||||
|
@ -46,7 +46,7 @@ export class TreeComponent {
|
||||||
C(2);
|
C(2);
|
||||||
C(3);
|
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, ' '));
|
t(1, i1(' ', ctx.data.value, ' '));
|
||||||
cR(2);
|
cR(2);
|
||||||
{
|
{
|
||||||
|
@ -114,7 +114,7 @@ export function TreeTpl(ctx: TreeNode, cm: boolean) {
|
||||||
}
|
}
|
||||||
e();
|
e();
|
||||||
}
|
}
|
||||||
s(1, 'background-color', b(ctx.depth % 2 ? '' : 'grey'));
|
sn(1, 'background-color', b(ctx.depth % 2 ? '' : 'grey'));
|
||||||
t(2, i1(' ', ctx.value, ' '));
|
t(2, i1(' ', ctx.value, ' '));
|
||||||
cR(3);
|
cR(3);
|
||||||
{
|
{
|
||||||
|
|
|
@ -63,7 +63,9 @@ export {
|
||||||
p as ɵp,
|
p as ɵp,
|
||||||
pD as ɵpD,
|
pD as ɵpD,
|
||||||
a as ɵa,
|
a as ɵa,
|
||||||
|
s as ɵs,
|
||||||
sn as ɵsn,
|
sn as ɵsn,
|
||||||
|
k as ɵk,
|
||||||
kn as ɵkn,
|
kn as ɵkn,
|
||||||
t as ɵt,
|
t as ɵt,
|
||||||
v as ɵv,
|
v as ɵv,
|
||||||
|
|
|
@ -50,4 +50,15 @@ export function getExported() { return exported; }
|
||||||
export function setExported(v) { exported = v; }
|
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];
|
||||||
|
}
|
||||||
|
```
|
|
@ -45,10 +45,12 @@ export {
|
||||||
containerRefreshEnd as cr,
|
containerRefreshEnd as cr,
|
||||||
|
|
||||||
elementAttribute as a,
|
elementAttribute as a,
|
||||||
|
elementClass as k,
|
||||||
elementClassNamed as kn,
|
elementClassNamed as kn,
|
||||||
elementEnd as e,
|
elementEnd as e,
|
||||||
elementProperty as p,
|
elementProperty as p,
|
||||||
elementStart as E,
|
elementStart as E,
|
||||||
|
elementStyle as s,
|
||||||
elementStyleNamed as sn,
|
elementStyleNamed as sn,
|
||||||
|
|
||||||
listener as L,
|
listener as L,
|
||||||
|
|
|
@ -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
|
* 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.
|
* Update a given style on an Element.
|
||||||
*
|
*
|
||||||
|
@ -908,22 +931,56 @@ export function elementStyleNamed<T>(
|
||||||
index: number, styleName: string, value: T | NO_CHANGE,
|
index: number, styleName: string, value: T | NO_CHANGE,
|
||||||
suffixOrSanitizer?: string | Sanitizer): void {
|
suffixOrSanitizer?: string | Sanitizer): void {
|
||||||
if (value !== NO_CHANGE) {
|
if (value !== NO_CHANGE) {
|
||||||
const lElement = data[index] as LElementNode;
|
const lElement: LElementNode = data[index];
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
isProceduralRenderer(renderer) ?
|
isProceduralRenderer(renderer) ?
|
||||||
renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) :
|
renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) :
|
||||||
lElement.native.style.removeProperty(styleName);
|
lElement.native['style'].removeProperty(styleName);
|
||||||
} else {
|
} else {
|
||||||
let strValue =
|
let strValue =
|
||||||
typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value);
|
typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value);
|
||||||
if (typeof suffixOrSanitizer == 'string') strValue = strValue + suffixOrSanitizer;
|
if (typeof suffixOrSanitizer == 'string') strValue = strValue + suffixOrSanitizer;
|
||||||
isProceduralRenderer(renderer) ?
|
isProceduralRenderer(renderer) ?
|
||||||
renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) :
|
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
|
//// Text
|
||||||
|
|
|
@ -121,6 +121,7 @@ export interface RNode {
|
||||||
export interface RElement extends RNode {
|
export interface RElement extends RNode {
|
||||||
style: RCssStyleDeclaration;
|
style: RCssStyleDeclaration;
|
||||||
classList: RDomTokenList;
|
classList: RDomTokenList;
|
||||||
|
className: string;
|
||||||
setAttribute(name: string, value: string): void;
|
setAttribute(name: string, value: string): void;
|
||||||
removeAttribute(name: string): void;
|
removeAttribute(name: string): void;
|
||||||
setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void;
|
setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void;
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
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 {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 * as $r3$ from '../../../src/core_render3_private_export';
|
||||||
import {renderComponent, toHtml} from '../render_util';
|
import {ComponentFixture, renderComponent, toHtml} from '../render_util';
|
||||||
|
|
||||||
/// See: `normative.md`
|
/// See: `normative.md`
|
||||||
describe('elements', () => {
|
describe('elements', () => {
|
||||||
|
@ -264,5 +264,36 @@ describe('elements', () => {
|
||||||
$r3$.ɵdetectChanges(comp);
|
$r3$.ɵdetectChanges(comp);
|
||||||
expect(toHtml(comp)).toEqual('<div class="" id="changed1" style="color: red;"></div>');
|
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>`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {LElementNode, LNode} from '../../src/render3/interfaces/node';
|
||||||
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
||||||
import {bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
|
import {bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
|
||||||
|
@ -58,7 +58,8 @@ describe('instructions', () => {
|
||||||
describe('elementStyleNamed', () => {
|
describe('elementStyleNamed', () => {
|
||||||
it('should use sanitizer function', () => {
|
it('should use sanitizer function', () => {
|
||||||
const t = new TemplateFixture(createDiv);
|
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.
|
// nothing is set because sanitizer suppresses it.
|
||||||
expect(t.html).toEqual('<div></div>');
|
expect(t.html).toEqual('<div></div>');
|
||||||
|
|
||||||
|
@ -70,4 +71,26 @@ describe('instructions', () => {
|
||||||
.toEqual('url("http://server")');
|
.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>');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue