fix(core): handle !important in style property value (#39603)
* Fixes that the Ivy styling logic wasn't accounting for `!important` in the property value. * Fixes that the default DOM renderer only sets `!important` on a property with a dash in its name. * Accounts for the `flags` parameter of `setStyle` in the server renderer. Fixes #35323. PR Close #39603
This commit is contained in:
parent
d1355caa74
commit
44763245e1
|
@ -12,7 +12,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 3037,
|
"runtime-es2015": 3037,
|
||||||
"main-es2015": 448922,
|
"main-es2015": 449483,
|
||||||
"polyfills-es2015": 52415
|
"polyfills-es2015": 52415
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -987,6 +987,13 @@ function applyContainer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(misko): Can't import RendererStyleFlags2.DashCase as it causes imports to be resolved
|
||||||
|
// in different order which causes failures. Duplicating for now.
|
||||||
|
const enum TempRendererStyleFlags2 {
|
||||||
|
Important = 1 << 0,
|
||||||
|
DashCase = 1 << 1
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes class/style to element.
|
* Writes class/style to element.
|
||||||
*
|
*
|
||||||
|
@ -1019,9 +1026,7 @@ export function applyStyling(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO(misko): Can't import RendererStyleFlags2.DashCase as it causes imports to be resolved
|
let flags = prop.indexOf('-') === -1 ? undefined : TempRendererStyleFlags2.DashCase as number;
|
||||||
// in different order which causes failures. Using direct constant as workaround for now.
|
|
||||||
const flags = prop.indexOf('-') == -1 ? undefined : 2 /* RendererStyleFlags2.DashCase */;
|
|
||||||
if (value == null /** || value === undefined */) {
|
if (value == null /** || value === undefined */) {
|
||||||
ngDevMode && ngDevMode.rendererRemoveStyle++;
|
ngDevMode && ngDevMode.rendererRemoveStyle++;
|
||||||
if (isProcedural) {
|
if (isProcedural) {
|
||||||
|
@ -1030,12 +1035,22 @@ export function applyStyling(
|
||||||
(rNode as HTMLElement).style.removeProperty(prop);
|
(rNode as HTMLElement).style.removeProperty(prop);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// A value is important if it ends with `!important`. The style
|
||||||
|
// parser strips any semicolons at the end of the value.
|
||||||
|
const isImportant = typeof value === 'string' ? value.endsWith('!important') : false;
|
||||||
|
|
||||||
|
if (isImportant) {
|
||||||
|
// !important has to be stripped from the value for it to be valid.
|
||||||
|
value = value.slice(0, -10);
|
||||||
|
flags! |= TempRendererStyleFlags2.Important;
|
||||||
|
}
|
||||||
|
|
||||||
ngDevMode && ngDevMode.rendererSetStyle++;
|
ngDevMode && ngDevMode.rendererSetStyle++;
|
||||||
if (isProcedural) {
|
if (isProcedural) {
|
||||||
(renderer as Renderer2).setStyle(rNode, prop, value, flags);
|
(renderer as Renderer2).setStyle(rNode, prop, value, flags);
|
||||||
} else {
|
} else {
|
||||||
ngDevMode && assertDefined((rNode as HTMLElement).style, 'HTMLElement expected');
|
ngDevMode && assertDefined((rNode as HTMLElement).style, 'HTMLElement expected');
|
||||||
(rNode as HTMLElement).style.setProperty(prop, value);
|
(rNode as HTMLElement).style.setProperty(prop, value, isImportant ? 'important' : '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {getElementClasses, getElementStyles, getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling';
|
import {getElementClasses, getElementStyles, getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling';
|
||||||
import {By, DomSanitizer, SafeStyle} from '@angular/platform-browser';
|
import {By, DomSanitizer, SafeStyle} from '@angular/platform-browser';
|
||||||
|
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||||
import {ivyEnabled, modifiedInIvy, onlyInIvy} from '@angular/private/testing';
|
import {ivyEnabled, modifiedInIvy, onlyInIvy} from '@angular/private/testing';
|
||||||
|
|
||||||
|
@ -801,6 +802,154 @@ describe('styling', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onlyInIvy('ViewEngine only supports !important when set through the `style` attribute')
|
||||||
|
.it('should set !important on a single property', () => {
|
||||||
|
// We go through `CSSStyleDeclaration.setProperty` to set `!important`. Values set through
|
||||||
|
// `setProperty` aren't propagated to the `style` attribute on IE11, even though the style
|
||||||
|
// is applied, so we have to skip the test there since we don't have a way of asserting.
|
||||||
|
if (browserDetection.isIE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({template: '<div [style.width]="width"></div>'})
|
||||||
|
class Cmp {
|
||||||
|
width!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
fixture.componentInstance.width = '50px !important';
|
||||||
|
fixture.detectChanges();
|
||||||
|
const html = fixture.nativeElement.innerHTML;
|
||||||
|
|
||||||
|
// We have to check the `style` attribute, because `element.style.prop` doesn't include
|
||||||
|
// `!important`. Use a regex, because the different renderers produce different whitespace.
|
||||||
|
expect(html).toMatch(/style=["|']width:\s*50px\s*!important/);
|
||||||
|
|
||||||
|
onlyInIvy('perf counters').expectPerfCounters({
|
||||||
|
rendererSetStyle: 1,
|
||||||
|
tNode: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onlyInIvy('ViewEngine only supports !important when set through the `style` attribute')
|
||||||
|
.it('should set !important that is not preceded by a space', () => {
|
||||||
|
// We go through `CSSStyleDeclaration.setProperty` to set `!important`. Values set through
|
||||||
|
// `setProperty` aren't propagated to the `style` attribute on IE11, even though the style
|
||||||
|
// is applied, so we have to skip the test there since we don't have a way of asserting.
|
||||||
|
if (browserDetection.isIE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({template: '<div [style.width]="width"></div>'})
|
||||||
|
class Cmp {
|
||||||
|
width!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
fixture.componentInstance.width = '50px!important';
|
||||||
|
fixture.detectChanges();
|
||||||
|
const html = fixture.nativeElement.innerHTML;
|
||||||
|
|
||||||
|
// We have to check the `style` attribute, because `element.style.prop` doesn't include
|
||||||
|
// `!important`. Use a regex, because the different renderers produce different whitespace.
|
||||||
|
expect(html).toMatch(/style=["|']width:\s*50px\s*!important/);
|
||||||
|
|
||||||
|
onlyInIvy('perf counters').expectPerfCounters({
|
||||||
|
rendererSetStyle: 1,
|
||||||
|
tNode: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onlyInIvy('ViewEngine only supports !important when set through the `style` attribute')
|
||||||
|
.it('should set !important on a dash-case property', () => {
|
||||||
|
// We go through `CSSStyleDeclaration.setProperty` to set `!important`. Values set through
|
||||||
|
// `setProperty` aren't propagated to the `style` attribute on IE11, even though the style
|
||||||
|
// is applied, so we have to skip the test there since we don't have a way of asserting.
|
||||||
|
if (browserDetection.isIE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({template: '<div [style.margin-right]="marginRight"></div>'})
|
||||||
|
class Cmp {
|
||||||
|
marginRight!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
fixture.componentInstance.marginRight = '5px !important';
|
||||||
|
fixture.detectChanges();
|
||||||
|
const html = fixture.nativeElement.innerHTML;
|
||||||
|
|
||||||
|
// We have to check the `style` attribute, because `element.style.prop` doesn't include
|
||||||
|
// `!important`. Use a regex, because the different renderers produce different whitespace.
|
||||||
|
expect(html).toMatch(/style=["|']margin-right:\s*5px\s*!important/);
|
||||||
|
|
||||||
|
onlyInIvy('perf counters').expectPerfCounters({
|
||||||
|
rendererSetStyle: 1,
|
||||||
|
tNode: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set !important on multiple properties', () => {
|
||||||
|
// We go through `CSSStyleDeclaration.setProperty` to set `!important`. Values set through
|
||||||
|
// `setProperty` aren't propagated to the `style` attribute on IE11, even though the style is
|
||||||
|
// applied, so we have to skip the test there since we don't have a way of asserting.
|
||||||
|
if (browserDetection.isIE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({template: '<div [style]="styles"></div>'})
|
||||||
|
class Cmp {
|
||||||
|
styles!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
fixture.componentInstance.styles = 'height: 25px !important; width: 50px !important;';
|
||||||
|
fixture.detectChanges();
|
||||||
|
const html = fixture.nativeElement.innerHTML;
|
||||||
|
|
||||||
|
// We have to check the `style` attribute, because `element.style.prop` doesn't include
|
||||||
|
// `!important`. Use a regex, because the different renderers produce different whitespace.
|
||||||
|
expect(html).toMatch(/style=["|']height:\s*25px\s*!important;\s*width:\s*50px\s*!important/);
|
||||||
|
|
||||||
|
onlyInIvy('perf counters').expectPerfCounters({
|
||||||
|
rendererSetStyle: 2,
|
||||||
|
tNode: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set !important if some properties are !important and other are not', () => {
|
||||||
|
// We go through `CSSStyleDeclaration.setProperty` to set `!important`. Values set through
|
||||||
|
// `setProperty` aren't propagated to the `style` attribute on IE11, even though the style is
|
||||||
|
// applied, so we have to skip the test there since we don't have a way of asserting.
|
||||||
|
if (browserDetection.isIE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({template: '<div [style]="styles"></div>'})
|
||||||
|
class Cmp {
|
||||||
|
styles!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
fixture.componentInstance.styles = 'height: 25px; width: 50px !important;';
|
||||||
|
fixture.detectChanges();
|
||||||
|
const html = fixture.nativeElement.innerHTML;
|
||||||
|
|
||||||
|
// We have to check the `style` attribute, because `element.style.prop` doesn't include
|
||||||
|
// `!important`. Use a regex, because the different renderers produce different whitespace.
|
||||||
|
expect(html).toMatch(/style=["|']height:\s*25px;\s*width:\s*50px\s*!important/);
|
||||||
|
|
||||||
|
onlyInIvy('perf counters').expectPerfCounters({
|
||||||
|
rendererSetStyle: 2,
|
||||||
|
tNode: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not write to the native element if a directive shadows the class input', () => {
|
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
|
// 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
|
// both by components with host elements and by directives on template nodes. In that case, the
|
||||||
|
|
|
@ -234,9 +234,8 @@ class DefaultDomRenderer2 implements Renderer2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
setStyle(el: any, style: string, value: any, flags: RendererStyleFlags2): void {
|
setStyle(el: any, style: string, value: any, flags: RendererStyleFlags2): void {
|
||||||
if (flags & RendererStyleFlags2.DashCase) {
|
if (flags & (RendererStyleFlags2.DashCase | RendererStyleFlags2.Important)) {
|
||||||
el.style.setProperty(
|
el.style.setProperty(style, value, flags & RendererStyleFlags2.Important ? 'important' : '');
|
||||||
style, value, !!(flags & RendererStyleFlags2.Important) ? 'important' : '');
|
|
||||||
} else {
|
} else {
|
||||||
el.style[style] = value;
|
el.style[style] = value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,6 +160,9 @@ class DefaultServerRenderer2 implements Renderer2 {
|
||||||
setStyle(el: any, style: string, value: any, flags: RendererStyleFlags2): void {
|
setStyle(el: any, style: string, value: any, flags: RendererStyleFlags2): void {
|
||||||
style = style.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
style = style.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||||
const styleMap = _readStyleAttribute(el);
|
const styleMap = _readStyleAttribute(el);
|
||||||
|
if (flags & RendererStyleFlags2.Important) {
|
||||||
|
value += ' !important';
|
||||||
|
}
|
||||||
styleMap[style] = value == null ? '' : value;
|
styleMap[style] = value == null ? '' : value;
|
||||||
_writeStyleAttribute(el, styleMap);
|
_writeStyleAttribute(el, styleMap);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue