` => `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;