@@ -22,11 +22,14 @@ import {
2222 ViewContainerRef ,
2323 Type ,
2424 PLATFORM_ID ,
25+ ComponentRef ,
2526} from '@angular/core' ;
2627import { DOCUMENT , isPlatformBrowser } from '@angular/common' ;
2728import { structuralStyles } from '@a2ui/web_core/styles/index' ;
2829import { Catalog } from './catalog' ;
30+ import { MessageProcessor } from '../data' ;
2931import { Types } from '../types' ;
32+ import { DynamicComponent } from './dynamic-component' ;
3033
3134@Directive ( {
3235 selector : '[a2ui-renderer]' ,
@@ -37,10 +40,15 @@ export class Renderer {
3740
3841 private readonly catalog = inject ( Catalog ) ;
3942 private readonly container = inject ( ViewContainerRef ) ;
43+ private readonly processor : MessageProcessor = inject ( MessageProcessor ) ;
4044
4145 readonly surfaceId = input . required < Types . SurfaceID > ( ) ;
4246 readonly component = input . required < Types . AnyComponentNode > ( ) ;
4347
48+ private currentId : string | null = null ;
49+ private currentType : string | null = null ;
50+ private currentComponentRef : ComponentRef < DynamicComponent < Types . AnyComponentNode > > | null = null ;
51+
4452 constructor ( ) {
4553 const platformId = inject ( PLATFORM_ID ) ;
4654 const document = inject ( DOCUMENT ) ;
@@ -53,10 +61,14 @@ export class Renderer {
5361 }
5462
5563 effect ( ( ) => {
56- const container = this . container ;
57- container . clear ( ) ;
64+ // Explicitly depend on the MessageProcessor's version signal. This ensures that the effect re-runs
65+ // whenever data model changes occur, even if the node's object reference remains identical
66+ // (as in the case of in-place mutations from local updates).
67+ this . processor . version ( ) ;
5868
5969 let node = this . component ( ) ;
70+ const surfaceId = this . surfaceId ( ) ;
71+
6072 // Handle v0.8 wrapped component format
6173 if ( ! node . type && ( node as any ) . component ) {
6274 const wrapped = ( node as any ) . component ;
@@ -70,48 +82,95 @@ export class Renderer {
7082 }
7183 }
7284
73- const config = this . catalog [ node . type ] ;
85+ const id = node . id ;
86+ const type = node . type ;
7487
88+ // Focus Loss Prevention:
89+ // If we have an existing component and its unique identity (ID and Type) hasn't changed,
90+ // we update its @Input () values in-place. This preserves the underlying DOM element,
91+ // maintaining focus, text selection, and cursor position.
92+ if ( this . currentComponentRef && this . currentId === id && this . currentType === type ) {
93+ this . updateInputs ( this . currentComponentRef , node , surfaceId ) ;
94+ return ;
95+ }
96+
97+ // Otherwise, clear and re-create the component because its identity has changed.
98+ const container = this . container ;
99+ container . clear ( ) ;
100+ this . currentComponentRef = null ;
101+ this . currentId = id ;
102+ this . currentType = type ;
103+
104+ const config = this . catalog [ node . type ] ;
75105 if ( ! config ) {
76106 console . error ( `Unknown component type: ${ node . type } ` ) ;
77107 return ;
78108 }
79109
80- this . render ( container , node , config ) ;
110+ this . render ( container , node , surfaceId , config ) ;
81111 } ) ;
82112 }
83113
84- private async render ( container : ViewContainerRef , node : Types . AnyComponentNode , config : any ) {
85- let componentType : Type < unknown > | null = null ;
114+ private render (
115+ container : ViewContainerRef ,
116+ node : Types . AnyComponentNode ,
117+ surfaceId : string ,
118+ config : any ,
119+ ) {
120+ const componentTypeOrPromise = this . resolveComponentType ( config ) ;
86121
122+ if ( componentTypeOrPromise instanceof Promise ) {
123+ componentTypeOrPromise . then ( ( componentType ) => {
124+ // Ensure we are still supposed to render this component
125+ if ( this . currentId === node . id && this . currentType === node . type ) {
126+ const componentRef = container . createComponent ( componentType ) as ComponentRef < DynamicComponent < Types . AnyComponentNode > > ;
127+ this . currentComponentRef = componentRef ;
128+ this . updateInputs ( componentRef , node , surfaceId ) ;
129+ }
130+ } ) ;
131+ } else if ( componentTypeOrPromise ) {
132+ const componentRef = container . createComponent ( componentTypeOrPromise ) as ComponentRef < DynamicComponent < Types . AnyComponentNode > > ;
133+ this . currentComponentRef = componentRef ;
134+ this . updateInputs ( componentRef , node , surfaceId ) ;
135+ }
136+ }
137+
138+ private resolveComponentType ( config : any ) : Type < unknown > | Promise < Type < unknown > > | null {
87139 if ( typeof config === 'function' ) {
88- const res = config ( ) ;
89- componentType = res instanceof Promise ? await res : res ;
140+ return config ( ) ;
90141 } else if ( typeof config === 'object' && config !== null ) {
91142 if ( typeof config . type === 'function' ) {
92- const res = config . type ( ) ;
93- componentType = res instanceof Promise ? await res : res ;
143+ return config . type ( ) ;
94144 } else {
95- componentType = config . type ;
145+ return config . type ;
96146 }
97147 }
148+ return null ;
149+ }
98150
99- if ( componentType ) {
100- const componentRef = container . createComponent ( componentType ) ;
101- componentRef . setInput ( 'surfaceId' , this . surfaceId ( ) ) ;
102- componentRef . setInput ( 'component' , node ) ;
103- componentRef . setInput ( 'weight' , node . weight ?? 0 ) ;
104-
105- const props = node . properties as Record < string , unknown > ;
106- for ( const [ key , value ] of Object . entries ( props ) ) {
107- try {
108- componentRef . setInput ( key , value ) ;
109- } catch ( e ) {
110- console . warn (
111- `[Renderer] Property "${ key } " could not be set on component ${ node . type } . If this property is required by the specification, ensure the component declares it as an input.` ,
112- ) ;
113- }
151+ /**
152+ * Updates the inputs of an existing component instance with the latest data from the node.
153+ * This is called during component reuse to keep the UI in sync without losing DOM state (like focus).
154+ */
155+ private updateInputs (
156+ componentRef : ComponentRef < DynamicComponent < Types . AnyComponentNode > > ,
157+ node : Types . AnyComponentNode ,
158+ surfaceId : string ,
159+ ) {
160+ componentRef . setInput ( 'surfaceId' , surfaceId ) ;
161+ componentRef . setInput ( 'component' , node ) ;
162+ componentRef . setInput ( 'weight' , node . weight ?? 0 ) ;
163+
164+ const props = node . properties as Record < string , unknown > ;
165+ for ( const [ key , value ] of Object . entries ( props ) ) {
166+ try {
167+ componentRef . setInput ( key , value ) ;
168+ } catch ( e ) {
169+ console . warn (
170+ `[Renderer] Property "${ key } " could not be set on component ${ node . type } . If this property is required by the specification, ensure the component declares it as an input.` ,
171+ ) ;
114172 }
115173 }
174+ componentRef . changeDetectorRef . markForCheck ( ) ;
116175 }
117176}
0 commit comments