@@ -7,66 +7,49 @@ import {
77 ElementRef ,
88 Renderer2 ,
99 OnInit ,
10+ OnDestroy ,
1011 Type ,
11- ViewContainerRef ,
1212 ViewEncapsulation ,
1313 Injector ,
1414 createComponent ,
1515 EnvironmentInjector ,
1616 ApplicationRef ,
17+ ComponentRef ,
18+ signal ,
1719} from '@angular/core' ;
1820import { NgComponentOutlet } from '@angular/common' ;
21+ import { isVoidElement } from '@storyblok/richtext' ;
1922import {
20- STORYBLOK_RICHTEXT_COMPONENTS ,
23+ StoryblokRichtextResolver ,
2124 type AngularRenderNode ,
2225 isTextNode ,
2326 isTagNode ,
2427 isComponentNode ,
2528} from './richtext.feature' ;
2629
27- /**
28- * Self-closing HTML elements that don't have children.
29- */
30- const VOID_ELEMENTS = new Set ( [
31- 'area' ,
32- 'base' ,
33- 'br' ,
34- 'col' ,
35- 'embed' ,
36- 'hr' ,
37- 'img' ,
38- 'input' ,
39- 'link' ,
40- 'meta' ,
41- 'param' ,
42- 'source' ,
43- 'track' ,
44- 'wbr' ,
45- ] ) ;
46-
4730/**
4831 * Recursively renders a single rich text AST node.
4932 *
5033 * This component handles three types of nodes:
5134 * - **Text nodes**: Rendered as plain text
5235 * - **Tag nodes**: Rendered as HTML elements with attributes
53- * - **Component nodes**: Rendered using custom Angular components
36+ * - **Component nodes**: Rendered using custom Angular components (supports lazy loading)
5437 *
5538 * The component uses Angular's `Renderer2` for SSR-safe DOM manipulation.
5639 *
57- * @internal This component is used internally by `RichTextComponent `.
40+ * @internal This component is used internally by `SbRichTextComponent `.
5841 * Users typically don't need to use this directly, but it's exported
5942 * for custom component implementations that need to render children.
6043 *
6144 * @example Rendering children in a custom component
6245 * ```typescript
6346 * @Component ({
6447 * selector: 'app-custom-link',
65- * imports: [RichTextNodeComponent ],
48+ * imports: [SbRichTextNodeComponent ],
6649 * template: `
6750 * <a [href]="href()">
6851 * @for (child of children(); track $index) {
69- * <sb-richtext -node [node]="child" />
52+ * <sb-rich-text -node [node]="child" />
7053 * }
7154 * </a>
7255 * `,
@@ -78,53 +61,40 @@ const VOID_ELEMENTS = new Set([
7861 * ```
7962 */
8063@Component ( {
81- selector : 'sb-richtext -node' ,
64+ selector : 'sb-rich-text -node' ,
8265 standalone : true ,
8366 changeDetection : ChangeDetectionStrategy . OnPush ,
8467 encapsulation : ViewEncapsulation . None ,
8568 imports : [ NgComponentOutlet ] ,
8669 template : `
87- @if (isComponentWithType ()) {
70+ @if (componentType ()) {
8871 <ng-container *ngComponentOutlet="componentType(); inputs: componentInputs()" />
8972 }
9073 ` ,
9174 styles : [
9275 `
93- sb-richtext -node {
76+ sb-rich-text -node {
9477 display: contents;
9578 }
9679 ` ,
9780 ] ,
9881} )
99- export class RichTextNodeComponent implements OnInit {
100- private readonly components = inject ( STORYBLOK_RICHTEXT_COMPONENTS ) ;
82+ export class SbRichTextNodeComponent implements OnInit , OnDestroy {
83+ private readonly resolver = inject ( StoryblokRichtextResolver ) ;
10184 private readonly renderer = inject ( Renderer2 ) ;
10285 private readonly el = inject ( ElementRef < HTMLElement > ) ;
10386 private readonly injector = inject ( Injector ) ;
10487 private readonly envInjector = inject ( EnvironmentInjector ) ;
10588 private readonly appRef = inject ( ApplicationRef ) ;
10689
90+ /** Track dynamically created child components for cleanup */
91+ private readonly childComponentRefs : ComponentRef < SbRichTextNodeComponent > [ ] = [ ] ;
92+
10793 /** The AST node to render */
10894 readonly node = input . required < AngularRenderNode > ( ) ;
10995
110- /**
111- * Whether this is a component node with a registered component type.
112- * Used to conditionally render the ngComponentOutlet in the template.
113- */
114- readonly isComponentWithType = computed ( ( ) => {
115- const node = this . node ( ) ;
116- if ( ! isComponentNode ( node ) ) return false ;
117- return this . components [ node . component ] != null ;
118- } ) ;
119-
120- /**
121- * The Angular component type to render for component nodes.
122- */
123- readonly componentType = computed < Type < unknown > | null > ( ( ) => {
124- const node = this . node ( ) ;
125- if ( ! isComponentNode ( node ) ) return null ;
126- return this . components [ node . component ] ?? null ;
127- } ) ;
96+ /** Resolved component type (supports lazy loading) */
97+ readonly componentType = signal < Type < unknown > | null > ( null ) ;
12898
12999 /**
130100 * The inputs to pass to the dynamic component.
@@ -160,10 +130,52 @@ export class RichTextNodeComponent implements OnInit {
160130 return ;
161131 }
162132
163- // Handle component nodes without a registered component (fallback: render children)
164- if ( isComponentNode ( node ) && ! this . componentType ( ) ) {
133+ // Handle component nodes
134+ if ( isComponentNode ( node ) ) {
135+ this . resolveAndRenderComponent ( node , hostElement ) ;
136+ }
137+ }
138+
139+ ngOnDestroy ( ) : void {
140+ // Clean up all dynamically created child components
141+ for ( const ref of this . childComponentRefs ) {
142+ this . appRef . detachView ( ref . hostView ) ;
143+ ref . destroy ( ) ;
144+ }
145+ this . childComponentRefs . length = 0 ;
146+ }
147+
148+ /**
149+ * Resolves and renders a component node, handling both eager and lazy loading.
150+ */
151+ private resolveAndRenderComponent (
152+ node : { component : string ; props : Record < string , unknown > ; children ?: AngularRenderNode [ ] } ,
153+ hostElement : HTMLElement ,
154+ ) : void {
155+ // Try synchronous resolution first (eager or cached)
156+ const syncComponent = this . resolver . getSync ( node . component as any ) ;
157+
158+ if ( syncComponent ) {
159+ this . componentType . set ( syncComponent ) ;
160+ return ;
161+ }
162+
163+ // Check if the component is registered at all
164+ if ( ! this . resolver . has ( node . component as any ) ) {
165+ // No component registered - render children as fallback
165166 this . renderChildren ( node . children ?? [ ] , hostElement ) ;
167+ return ;
166168 }
169+
170+ // Lazy component - resolve asynchronously
171+ this . resolver . resolve ( node . component as any ) . then ( ( component ) => {
172+ if ( component ) {
173+ this . componentType . set ( component ) ;
174+ } else {
175+ // Fallback: render children if component couldn't be resolved
176+ this . renderChildren ( node . children ?? [ ] , hostElement ) ;
177+ }
178+ } ) ;
167179 }
168180
169181 /**
@@ -195,24 +207,27 @@ export class RichTextNodeComponent implements OnInit {
195207 // Append to parent element
196208 this . renderer . appendChild ( parent , element ) ;
197209
198- // Render children inside the element (skip for void elements)
199- if ( ! VOID_ELEMENTS . has ( node . tag ) && node . children ?. length ) {
210+ // Render children inside the element (skip for void elements like img, br, hr )
211+ if ( ! isVoidElement ( node . tag ) && node . children ?. length ) {
200212 this . renderChildren ( node . children , element ) ;
201213 }
202214 }
203215
204216 /**
205- * Recursively renders child nodes by creating RichTextNodeComponent instances.
217+ * Recursively renders child nodes by creating SbRichTextNodeComponent instances.
206218 * Uses createComponent from @angular/core for proper SSR support.
207219 */
208220 private renderChildren ( children : AngularRenderNode [ ] , parent : HTMLElement | Element ) : void {
209221 for ( const child of children ) {
210222 // Create the component using Angular's createComponent
211- const componentRef = createComponent ( RichTextNodeComponent , {
223+ const componentRef = createComponent ( SbRichTextNodeComponent , {
212224 environmentInjector : this . envInjector ,
213225 elementInjector : this . injector ,
214226 } ) ;
215227
228+ // Track for cleanup
229+ this . childComponentRefs . push ( componentRef ) ;
230+
216231 // Set the input
217232 componentRef . setInput ( 'node' , child ) ;
218233
0 commit comments