Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions apps/nativescript-demo-ng/src/tests/css-variables.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
Component,
ElementRef,
inject,
NO_ERRORS_SCHEMA,
Renderer2,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { NativeScriptCommonModule } from '@nativescript/angular';
import { Color, Label } from '@nativescript/core';

// Styles are written in the namespaced form Angular >= 22.1 compiles custom properties to,
// so these specs hold regardless of the compiler version the demo app builds with.

@Component({
template: `<Label #label class="css-var-emulated"></Label>`,
styles: ['.css-var-emulated { --%NS%probe-emulated: #ff0000; background-color: var(--%NS%probe-emulated); }'],
imports: [NativeScriptCommonModule],
schemas: [NO_ERRORS_SCHEMA],
})
class EmulatedCssVarComponent {
@ViewChild('label', { static: true }) label: ElementRef<Label>;
renderer = inject(Renderer2);
}

@Component({
template: `<Label #label class="css-var-none"></Label>`,
styles: ['.css-var-none { --%NS%probe-none: #00ff00; background-color: var(--%NS%probe-none); }'],
encapsulation: ViewEncapsulation.None,
imports: [NativeScriptCommonModule],
schemas: [NO_ERRORS_SCHEMA],
})
class NoneCssVarComponent {
@ViewChild('label', { static: true }) label: ElementRef<Label>;
}

describe('CSS custom properties', () => {
beforeEach(() => TestBed.configureTestingModule({ imports: [EmulatedCssVarComponent, NoneCssVarComponent] }));

it('applies namespaced custom properties in emulated component styles', () => {
const fixture = TestBed.createComponent(EmulatedCssVarComponent);
fixture.detectChanges();
const label = fixture.componentInstance.label.nativeElement;
expect(label.style.getCssVariable('--probe-emulated')).toBe('#ff0000');
expect(label.style.backgroundColor?.hex).toBe(new Color('#ff0000').hex);
});

it('applies namespaced custom properties in unencapsulated component styles', () => {
const fixture = TestBed.createComponent(NoneCssVarComponent);
fixture.detectChanges();
const label = fixture.componentInstance.label.nativeElement;
expect(label.style.getCssVariable('--probe-none')).toBe('#00ff00');
expect(label.style.backgroundColor?.hex).toBe(new Color('#00ff00').hex);
});

it('sets and removes namespaced custom properties through the renderer', () => {
const fixture = TestBed.createComponent(EmulatedCssVarComponent);
fixture.detectChanges();
const { label, renderer } = fixture.componentInstance;
renderer.setStyle(label.nativeElement, '--%NS%bound', 'blue');
expect(label.nativeElement.style.getCssVariable('--bound')).toBe('blue');
renderer.removeStyle(label.nativeElement, '--%NS%bound');
expect(label.nativeElement.style.getCssVariable('--bound')).toBeNull();
});
});
19 changes: 17 additions & 2 deletions packages/angular/src/lib/nativescript-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ const addStyleToCss = profile(
},
);

// Angular >= 22.1 compiles custom properties in component styles and style bindings
// to `--%NS%name`, leaving the renderer to substitute a namespace. NativeScript has no
// namespacing, so drop the placeholder to match Angular's default (an empty namespace).
const CSS_VAR_NAMESPACE_REGEX = /%NS%/g;

function stripCssVarNamespace(input: string): string {
return input.replace(CSS_VAR_NAMESPACE_REGEX, '');
}

function runInRootZone<T>(fn: () => T): T {
if (typeof Zone === 'undefined') {
return fn();
Expand Down Expand Up @@ -156,7 +165,7 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
}

if (type.encapsulation === ViewEncapsulation.None) {
type.styles.map((s) => s.toString()).forEach((v) => addStyleToCss(v, this.rootModuleID));
type.styles.map((s) => stripCssVarNamespace(s.toString())).forEach((v) => addStyleToCss(v, this.rootModuleID));
renderer = this.defaultRenderer;
} else {
runInInjectionContext(this.injector, () => {
Expand Down Expand Up @@ -404,6 +413,9 @@ class NativeScriptRenderer implements Renderer2 {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog(`NativeScriptRenderer.setStyle: ${el}, ${style} = ${value}`);
}
if (style.startsWith('--')) {
style = stripCssVarNamespace(style);
}
this.viewUtil.setStyle(el, style, value);
}
@inRootZone()
Expand All @@ -412,6 +424,9 @@ class NativeScriptRenderer implements Renderer2 {
if (NativeScriptDebug.enabled) {
NativeScriptDebug.rendererLog('NativeScriptRenderer.removeStyle: ${styleName}');
}
if (style.startsWith('--')) {
style = stripCssVarNamespace(style);
}
this.viewUtil.removeStyle(el, style);
}
@inRootZone()
Expand Down Expand Up @@ -522,7 +537,7 @@ export class EmulatedRenderer extends NativeScriptRenderer {
private addStyles(styles: (string | any[])[], componentId: string) {
styles
.map((s) => s.toString())
.map((s) => replaceNgAttribute(s, componentId))
.map((s) => stripCssVarNamespace(replaceNgAttribute(s, componentId)))
.forEach((s) => addScopedStyleToCss(s, this.rootModuleId));
}
}
Loading