Skip to content

Commit 496b051

Browse files
committed
fix(hmr): emulated styleUrl handling
1 parent 925c44f commit 496b051

1 file changed

Lines changed: 71 additions & 0 deletions

File tree

packages/angular/src/lib/nativescript-renderer.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ function modifiesDom() {
106106

107107
export class NativeScriptRendererFactory implements RendererFactory2 {
108108
private componentRenderers = new Map<string, Renderer2>();
109+
// Signature of the styles last applied for each component `type.id`. Used to
110+
// detect a `styleUrls`/`styles` change across an `replaceMetadata` HMR
111+
// update so the cached renderer can re-apply the new (scoped) styles - the
112+
// renderer cache below otherwise short-circuits `addStyles`, so a component
113+
// style edit would never take effect without a full re-bootstrap.
114+
private componentStyleSignatures = new Map<string, string>();
109115
private defaultRenderer: Renderer2;
110116
// backwards compatibility with RadListView
111117
private rootView = inject(APP_ROOT_VIEW);
@@ -151,6 +157,25 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
151157
renderer.applyToHost(hostElement);
152158
}
153159

160+
// HMR: a component `styleUrls`/`styles` edit recompiles the component
161+
// metadata and `replaceMetadata` recreates its views, which re-enters
162+
// `createRenderer` with the SAME `type.id` but NEW `type.styles`. The
163+
// cache hit above would otherwise return the renderer whose one-time
164+
// `addStyles` already ran with the OLD styles, so the change would never
165+
// render. When the style signature changed, re-apply: emulated styles
166+
// are re-scoped + re-added (same selector/specificity -> later wins);
167+
// None-encapsulation styles are re-added globally. Both keep the shared
168+
// `rootModuleID` tag so module teardown still removes them.
169+
const styleSignature = this.styleSignature(type.styles);
170+
if (this.componentStyleSignatures.get(type.id) !== styleSignature) {
171+
this.componentStyleSignatures.set(type.id, styleSignature);
172+
if (renderer instanceof EmulatedRenderer) {
173+
renderer.reapplyStyles(type.styles);
174+
} else {
175+
this.reapplyGlobalStyles(type.styles);
176+
}
177+
}
178+
154179
return renderer;
155180
}
156181

@@ -165,8 +190,31 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
165190
}
166191

167192
this.componentRenderers.set(type.id, renderer);
193+
this.componentStyleSignatures.set(type.id, this.styleSignature(type.styles));
168194
return renderer;
169195
}
196+
197+
// Stable signature of a component's styles, used to detect HMR style edits.
198+
private styleSignature(styles: (string | any[])[]): string {
199+
try {
200+
return (styles || []).map((s) => s.toString()).join("\n");
201+
} catch {
202+
return '';
203+
}
204+
}
205+
206+
// Re-apply ViewEncapsulation.None component styles (global, unscoped) on an
207+
// HMR style edit and re-trigger styling on the live view tree.
208+
private reapplyGlobalStyles(styles: (string | any[])[]): void {
209+
try {
210+
styles.map((s) => s.toString()).forEach((v) => addStyleToCss(v, this.rootModuleID));
211+
Application.getRootView()?._onCssStateChange();
212+
} catch (err) {
213+
if (NativeScriptDebug.enabled) {
214+
NativeScriptDebug.rendererLog(`reapplyGlobalStyles failed: ${err}`);
215+
}
216+
}
217+
}
170218
begin() {
171219
if (__APPLE__ && this.wrapCdInTransaction) {
172220
if (this.cdDepth > 0) {
@@ -484,17 +532,40 @@ const addScopedStyleToCss = profile(
484532
export class EmulatedRenderer extends NativeScriptRenderer {
485533
private contentAttr: string;
486534
private hostAttr: string;
535+
private componentId: string;
487536
private rootModuleId = inject(NATIVESCRIPT_ROOT_MODULE_ID);
488537

489538
constructor(component: RendererType2, rootView: View) {
490539
super(rootView);
491540

492541
const componentId = component.id.replace(ATTR_SANITIZER, '_');
542+
this.componentId = componentId;
493543
this.contentAttr = replaceNgAttribute(CONTENT_ATTR, componentId);
494544
this.hostAttr = replaceNgAttribute(HOST_ATTR, componentId);
495545
this.addStyles(component.styles, componentId);
496546
}
497547

548+
/**
549+
* Re-apply this component's emulated-scoped styles after an HMR
550+
* `styleUrls`/`styles` edit. The renderer is cached per component type id
551+
* (see `NativeScriptRendererFactory.createRenderer`), so the constructor's
552+
* one-time `addStyles` never re-runs on `replaceMetadata` - without this
553+
* the new styles never reach the device. The freshly-compiled rules are
554+
* re-scoped to this renderer's component id (so the existing views, which
555+
* carry that `_ngcontent` attribute, match) and re-added under the same
556+
* `rootModuleId` tag; since they share the previous rules' selector and
557+
* specificity, the later-added values win. We then re-trigger styling on
558+
* the live view tree so the change paints without a re-bootstrap.
559+
*/
560+
reapplyStyles(styles: (string | any[])[]): void {
561+
this.addStyles(styles, this.componentId);
562+
try {
563+
Application.getRootView()?._onCssStateChange();
564+
} catch {
565+
// best-effort restyle; never let an HMR style re-apply throw
566+
}
567+
}
568+
498569
applyToHost(view: NgView) {
499570
super.setAttribute(view, this.hostAttr, '');
500571
}

0 commit comments

Comments
 (0)