Skip to content

Commit dfd63b0

Browse files
committed
fix(hmr): emulated styleUrl handling
1 parent ff84f87 commit dfd63b0

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
@@ -107,6 +107,12 @@ function modifiesDom() {
107107

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

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

@@ -166,8 +191,31 @@ export class NativeScriptRendererFactory implements RendererFactory2 {
166191
}
167192

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

492541
constructor(component: RendererType2, rootView: View) {
493542
super(rootView);
494543

495544
const componentId = component.id.replace(ATTR_SANITIZER, '_');
545+
this.componentId = componentId;
496546
this.contentAttr = replaceNgAttribute(CONTENT_ATTR, componentId);
497547
this.hostAttr = replaceNgAttribute(HOST_ATTR, componentId);
498548
this.addStyles(component.styles, componentId);
499549
}
500550

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

0 commit comments

Comments
 (0)