11import {
22 AfterViewInit ,
3+ ChangeDetectorRef ,
34 Component ,
45 ContentChild ,
56 Input ,
7+ OnDestroy ,
68 TemplateRef ,
79} from '@angular/core' ;
810import { CalendarApp } from '@schedule-x/calendar' ;
@@ -34,7 +36,7 @@ export const randomStringId = () =>
3436 ` ,
3537 styles : `` ,
3638} )
37- export class CalendarComponent implements AfterViewInit {
39+ export class CalendarComponent implements AfterViewInit , OnDestroy {
3840 @Input ( ) calendarApp : CalendarApp ;
3941
4042 @ContentChild ( 'timeGridEvent' ) timeGridEvent : TemplateRef < any > ;
@@ -55,6 +57,9 @@ export class CalendarComponent implements AfterViewInit {
5557 customComponentsMeta : CustomComponentsMeta = [ ] ;
5658
5759 public calendarElementId = randomStringId ( ) ;
60+ private isDestroyed = false ;
61+
62+ constructor ( private changeDetectorRef : ChangeDetectorRef ) { }
5863
5964 getTemplate ( componentName : string ) : TemplateRef < any > {
6065 if ( componentName === 'timeGridEvent' ) return this . timeGridEvent ;
@@ -84,7 +89,7 @@ export class CalendarComponent implements AfterViewInit {
8489 throw new Error ( `No template found for component name: ${ componentName } ` ) ;
8590 }
8691
87- ngAfterViewInit ( ) {
92+ async ngAfterViewInit ( ) {
8893 if ( typeof window !== 'object' ) return ;
8994
9095 const calendarElement = document ?. getElementById ( this . calendarElementId ) ;
@@ -96,10 +101,23 @@ export class CalendarComponent implements AfterViewInit {
96101 return ;
97102 }
98103
104+ // Schedule-X renders synchronously and can request custom component portals
105+ // during that render. If this happens inside Angular's current
106+ // AfterViewInit check, dev mode sees customComponentsMeta change from
107+ // [] to a wrapper element and throws ExpressionChangedAfterItHasBeenCheckedError.
108+ // Deferring one microtask starts the external render after this check settles.
109+ await Promise . resolve ( ) ;
110+
111+ if ( this . isDestroyed ) return ;
112+
99113 this . setCustomComponentFns ( ) ;
100114 this . calendarApp ?. render ( calendarElement ) ;
101115 }
102116
117+ ngOnDestroy ( ) {
118+ this . isDestroyed = true ;
119+ }
120+
103121 private setCustomComponentFns ( ) {
104122 if ( this . timeGridEvent ) {
105123 this . calendarApp . _setCustomComponentFn (
@@ -213,6 +231,8 @@ export class CalendarComponent implements AfterViewInit {
213231 }
214232
215233 setCustomComponentMeta = ( component : CustomComponentMeta ) => {
234+ if ( this . isDestroyed ) return ;
235+
216236 const wrapperWasDetached = ! (
217237 component . wrapperElement instanceof HTMLElement
218238 ) ;
@@ -243,5 +263,10 @@ export class CalendarComponent implements AfterViewInit {
243263 }
244264
245265 this . customComponentsMeta = [ ...newCustomComponents , component ] ;
266+
267+ // Custom component callbacks originate from Schedule-X/Preact, outside of
268+ // Angular's normal template event flow. Run a local detection pass so the
269+ // portal created above is reconciled immediately.
270+ this . changeDetectorRef . detectChanges ( ) ;
246271 } ;
247272}
0 commit comments