diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 6b3810e74597c0..c8d4ebae53f90c 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -57,6 +57,7 @@ import {ComponentDef, DirectiveDef, HostDirectiveDefs} from './interfaces/defini import {InputFlags} from './interfaces/input_flags'; import { NodeInputBindings, + TAttributes, TContainerNode, TElementContainerNode, TElementNode, @@ -89,6 +90,7 @@ import {getComponentLViewByIndex, getNativeByTNode, getTNode} from './util/view_ import {ViewRef} from './view_ref'; import {ChainedInjector} from './chained_injector'; import {unregisterLView} from './interfaces/lview_tracking'; +import {AttributeMarker} from './interfaces/attribute_marker'; export class ComponentFactoryResolver extends AbstractComponentFactoryResolver { /** @@ -499,10 +501,29 @@ function createRootComponentTNode(lView: LView, rNode: RNode): TElementNode { ngDevMode && assertIndexInRange(lView, index); lView[index] = rNode; + let attr: TAttributes | null = null; + if (rNode && 'classList' in rNode && (rNode as any).classList?.length) { + attr = [AttributeMarker.Classes, ...(Array.from((rNode as any).classList) as string[])]; + } + if (rNode && 'getAttribute' in rNode && (rNode as any).getAttribute('style')?.length) { + attr = attr?.length ? [...attr, AttributeMarker.Styles] : [AttributeMarker.Styles]; + const styleAttribute = (rNode as any).getAttribute('style'); + + const styles: string[] = styleAttribute.split(';'); + + styles.forEach((style) => { + const [property, value] = style.split(':').map((s) => s.trim()); + + if (property && value) { + attr!.push(property, value); + } + }); + } + // '#host' is added here as we don't know the real host DOM name (we don't want to read it) and at // the same time we want to communicate the debug `TNode` that this is a special `TNode` // representing a host element. - return getOrCreateTNode(tView, index, TNodeType.Element, '#host', null); + return getOrCreateTNode(tView, index, TNodeType.Element, '#host', attr); } /** @@ -576,7 +597,9 @@ function applyRootComponentStyling( for (const def of rootDirectives) { tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, def.hostAttrs); } - + if (tNode.attrs !== null) { + tNode.mergedAttrs = mergeHostAttrs(tNode.mergedAttrs, tNode.attrs); + } if (tNode.mergedAttrs !== null) { computeStaticStyling(tNode, tNode.mergedAttrs, true); diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 952144b58f221e..fd067a4f028774 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -1280,10 +1280,22 @@ export function setupStaticAttributes(renderer: Renderer, element: RElement, tNo } if (classes !== null) { - writeDirectClass(renderer, element, classes); + const elementClasses = element.className.split + ? (element.className + .split(' ') + .filter((item) => !classes.includes(item)) + .join(' ') ?? '') + : ''; + writeDirectClass(renderer, element, `${classes} ${elementClasses}`.trim()); } if (styles !== null) { - writeDirectStyle(renderer, element, styles); + const elementStyles = + element + .getAttribute('style') + ?.split(';') + .filter((item) => !styles.includes(item)) + .join(';') ?? ''; + writeDirectStyle(renderer, element, `${styles} ${elementStyles}`.trim()); } } diff --git a/packages/core/test/acceptance/bootstrap_spec.ts b/packages/core/test/acceptance/bootstrap_spec.ts index 004f5f9ae66c9d..239d2e6b1c4484 100644 --- a/packages/core/test/acceptance/bootstrap_spec.ts +++ b/packages/core/test/acceptance/bootstrap_spec.ts @@ -21,6 +21,7 @@ import { ViewEncapsulation, ɵNoopNgZone, ɵZONELESS_ENABLED, + ElementRef, } from '@angular/core'; import {bootstrapApplication, BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; @@ -82,6 +83,31 @@ describe('bootstrap', () => { }), ); + it( + 'should preserve static class and styles on root component', + withBody('', async () => { + @Component({ + selector: 'test-cmp', + standalone: true, + template: '(test)', + host: { + class: 'baar', + style: 'width: 100%;', + }, + }) + class TestCmp { + constructor(public element: ElementRef) {} + } + const appRef = await bootstrapApplication(TestCmp); + expect(appRef.components[0].instance.element.nativeElement.className).toBe('baar foo'); + expect(appRef.components[0].instance.element.nativeElement.getAttribute('style')).toBe( + 'width: 100%; height: 100%;', + ); + + appRef.destroy(); + }), + ); + describe('options', () => { function createComponentAndModule( options: {