forked from EVOgeek/Vmeda.Online
		
	MOBILE-3814 chore: Improve component registry wait for ready
This commit is contained in:
		
							parent
							
								
									b1461680a8
								
							
						
					
					
						commit
						4cdc6b9dd6
					
				
							
								
								
									
										24
									
								
								src/core/classes/async-component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/core/classes/async-component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component that is not rendered immediately after being mounted. | ||||||
|  |  */ | ||||||
|  | export interface AsyncComponent { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Wait until the component is fully rendered and ready. | ||||||
|  |      */ | ||||||
|  |     ready(): Promise<void>; | ||||||
|  | } | ||||||
| @ -20,6 +20,7 @@ import { CoreAnimations } from '@components/animations'; | |||||||
| import { Translate } from '@singletons'; | import { Translate } from '@singletons'; | ||||||
| import { CoreComponentsRegistry } from '@singletons/components-registry'; | import { CoreComponentsRegistry } from '@singletons/components-registry'; | ||||||
| import { CorePromisedValue } from '@classes/promised-value'; | import { CorePromisedValue } from '@classes/promised-value'; | ||||||
|  | import { AsyncComponent } from '@classes/async-component'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Component to show a loading spinner and message while data is being loaded. |  * Component to show a loading spinner and message while data is being loaded. | ||||||
| @ -47,7 +48,7 @@ import { CorePromisedValue } from '@classes/promised-value'; | |||||||
|     styleUrls: ['loading.scss'], |     styleUrls: ['loading.scss'], | ||||||
|     animations: [CoreAnimations.SHOW_HIDE], |     animations: [CoreAnimations.SHOW_HIDE], | ||||||
| }) | }) | ||||||
| export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { | export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, AsyncComponent { | ||||||
| 
 | 
 | ||||||
|     @Input() hideUntil: unknown; // Determine when should the contents be shown.
 |     @Input() hideUntil: unknown; // Determine when should the contents be shown.
 | ||||||
|     @Input() message?: string; // Message to show while loading.
 |     @Input() message?: string; // Message to show while loading.
 | ||||||
| @ -58,7 +59,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { | |||||||
|     uniqueId: string; |     uniqueId: string; | ||||||
|     protected element: HTMLElement; // Current element.
 |     protected element: HTMLElement; // Current element.
 | ||||||
|     loaded = false; // Only comes true once.
 |     loaded = false; // Only comes true once.
 | ||||||
|     protected firstLoadedPromise = new CorePromisedValue<string>(); |     protected onReadyPromise = new CorePromisedValue<void>(); | ||||||
| 
 | 
 | ||||||
|     constructor(element: ElementRef) { |     constructor(element: ElementRef) { | ||||||
|         this.element = element.nativeElement; |         this.element = element.nativeElement; | ||||||
| @ -108,7 +109,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { | |||||||
| 
 | 
 | ||||||
|         if (!this.loaded && loaded) { |         if (!this.loaded && loaded) { | ||||||
|             this.loaded = true; // Only comes true once.
 |             this.loaded = true; // Only comes true once.
 | ||||||
|             this.firstLoadedPromise.resolve(this.uniqueId); |             this.onReadyPromise.resolve(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Event has been deprecated since app 4.0.
 |         // Event has been deprecated since app 4.0.
 | ||||||
| @ -119,12 +120,10 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Wait the loading to finish. |      * @inheritdoc | ||||||
|      * |  | ||||||
|      * @return Promise resolved with the uniqueId when done. |  | ||||||
|      */ |      */ | ||||||
|     async whenLoaded(): Promise<string> { |     async ready(): Promise<void> { | ||||||
|         return await this.firstLoadedPromise; |         return await this.onReadyPromise; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -66,7 +66,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|         await this.domPromise; |         await this.domPromise; | ||||||
|         await this.waitLoadingsDone(); |         await this.waitLoadingsDone(); | ||||||
|         await this.waitFormatTextsRendered(this.element); |         await this.waitFormatTextsRendered(); | ||||||
| 
 | 
 | ||||||
|         this.content = this.element.closest('ion-content'); |         this.content = this.element.closest('ion-content'); | ||||||
| 
 | 
 | ||||||
| @ -156,15 +156,9 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Wait until all <core-format-text> children inside the element are done rendering. |      * Wait until all <core-format-text> children inside the element are done rendering. | ||||||
|      * |  | ||||||
|      * @param element Element. |  | ||||||
|      */ |      */ | ||||||
|     protected async waitFormatTextsRendered(element: Element): Promise<void> { |     protected async waitFormatTextsRendered(): Promise<void> { | ||||||
|         await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>( |         await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective); | ||||||
|             element, |  | ||||||
|             'core-format-text', |  | ||||||
|             'rendered', |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -210,13 +204,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { | |||||||
|         const scrollElement = await this.ionContent.getScrollElement(); |         const scrollElement = await this.ionContent.getScrollElement(); | ||||||
| 
 | 
 | ||||||
|         await Promise.all([ |         await Promise.all([ | ||||||
|             await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent> |             await CoreComponentsRegistry.waitComponentsReady(scrollElement, 'core-loading', CoreLoadingComponent), | ||||||
|             (scrollElement, 'core-loading', 'whenLoaded'), |             await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-loading', CoreLoadingComponent), | ||||||
|             await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>( |  | ||||||
|                 this.element, |  | ||||||
|                 'core-loading', |  | ||||||
|                 'whenLoaded', |  | ||||||
|             ), |  | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -332,15 +332,13 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest | |||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Wait until all <core-loading> children inside the page. |      * Wait until all <core-loading> children inside the page. | ||||||
|      * |  | ||||||
|      * @return Promise resolved when loadings are done. |  | ||||||
|      */ |      */ | ||||||
|     protected async waitLoadingsDone(): Promise<void> { |     protected async waitLoadingsDone(): Promise<void> { | ||||||
|         await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>( |         if (!this.page) { | ||||||
|             this.page, |             return; | ||||||
|             'core-loading', |         } | ||||||
|             'whenLoaded', | 
 | ||||||
|         ); |         await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -350,11 +348,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest | |||||||
|      * @return Promise resolved when texts are rendered. |      * @return Promise resolved when texts are rendered. | ||||||
|      */ |      */ | ||||||
|     protected async waitFormatTextsRendered(element: Element): Promise<void> { |     protected async waitFormatTextsRendered(element: Element): Promise<void> { | ||||||
|         await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>( |         await CoreComponentsRegistry.waitComponentsReady(element, 'core-format-text', CoreFormatTextDirective); | ||||||
|             element, |  | ||||||
|             'core-format-text', |  | ||||||
|             'rendered', |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -16,7 +16,6 @@ import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; | |||||||
| import { CoreCancellablePromise } from '@classes/cancellable-promise'; | import { CoreCancellablePromise } from '@classes/cancellable-promise'; | ||||||
| import { CoreLoadingComponent } from '@components/loading/loading'; | import { CoreLoadingComponent } from '@components/loading/loading'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; |  | ||||||
| import { Translate } from '@singletons'; | import { Translate } from '@singletons'; | ||||||
| import { CoreComponentsRegistry } from '@singletons/components-registry'; | import { CoreComponentsRegistry } from '@singletons/components-registry'; | ||||||
| import { CoreEventObserver } from '@singletons/events'; | import { CoreEventObserver } from '@singletons/events'; | ||||||
| @ -103,28 +102,18 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|         const page = this.element.closest('.ion-page'); |         const page = this.element.closest('.ion-page'); | ||||||
| 
 | 
 | ||||||
|         await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(page, 'core-loading', 'whenLoaded'); |         if (!page) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Wait until all <core-format-text> children inside the element are done rendering. |      * Wait until all <core-format-text> children inside the element are done rendering. | ||||||
|      * |  | ||||||
|      * @param element Element. |  | ||||||
|      */ |      */ | ||||||
|     protected async waitFormatTextsRendered(element: Element): Promise<void> { |     protected async waitFormatTextsRendered(): Promise<void> { | ||||||
|         let formatTextElements: HTMLElement[] = []; |         await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective); | ||||||
| 
 |  | ||||||
|         if (this.element.tagName == 'CORE-FORMAT-TEXT') { |  | ||||||
|             formatTextElements = [this.element]; |  | ||||||
|         } else { |  | ||||||
|             formatTextElements = Array.from(element.querySelectorAll('core-format-text')); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const formatTexts = formatTextElements.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective)); |  | ||||||
| 
 |  | ||||||
|         await Promise.all(formatTexts.map(formatText => formatText?.rendered())); |  | ||||||
| 
 |  | ||||||
|         await CoreUtils.nextTick(); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -134,7 +123,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { | |||||||
|         // Remove max-height (if any) to calculate the real height.
 |         // Remove max-height (if any) to calculate the real height.
 | ||||||
|         this.element.classList.add('collapsible-loading-height'); |         this.element.classList.add('collapsible-loading-height'); | ||||||
| 
 | 
 | ||||||
|         await this.waitFormatTextsRendered(this.element); |         await this.waitFormatTextsRendered(); | ||||||
| 
 | 
 | ||||||
|         this.expandedHeight = this.element.getBoundingClientRect().height; |         this.expandedHeight = this.element.getBoundingClientRect().height; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -43,6 +43,7 @@ import { CoreSubscriptions } from '@singletons/subscriptions'; | |||||||
| import { CoreComponentsRegistry } from '@singletons/components-registry'; | import { CoreComponentsRegistry } from '@singletons/components-registry'; | ||||||
| import { CoreCollapsibleItemDirective } from './collapsible-item'; | import { CoreCollapsibleItemDirective } from './collapsible-item'; | ||||||
| import { CoreCancellablePromise } from '@classes/cancellable-promise'; | import { CoreCancellablePromise } from '@classes/cancellable-promise'; | ||||||
|  | import { AsyncComponent } from '@classes/async-component'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective |  * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective | ||||||
| @ -56,7 +57,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise'; | |||||||
| @Directive({ | @Directive({ | ||||||
|     selector: 'core-format-text', |     selector: 'core-format-text', | ||||||
| }) | }) | ||||||
| export class CoreFormatTextDirective implements OnChanges, OnDestroy { | export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncComponent { | ||||||
| 
 | 
 | ||||||
|     @ViewChild(CoreCollapsibleItemDirective) collapsible?: CoreCollapsibleItemDirective; |     @ViewChild(CoreCollapsibleItemDirective) collapsible?: CoreCollapsibleItemDirective; | ||||||
| 
 | 
 | ||||||
| @ -137,9 +138,9 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Wait until the text is fully rendered. |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     async rendered(): Promise<void> { |     async ready(): Promise<void> { | ||||||
|         if (!this.element.classList.contains('core-format-text-loading')) { |         if (!this.element.classList.contains('core-format-text-loading')) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -294,8 +294,11 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn | |||||||
|         await this.domPromise; |         await this.domPromise; | ||||||
| 
 | 
 | ||||||
|         const page = this.element.closest('.ion-page'); |         const page = this.element.closest('.ion-page'); | ||||||
|  |         if (!page) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(page, 'core-loading', 'whenLoaded'); |         await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component } from '@angular/core'; | import { Component } from '@angular/core'; | ||||||
|  | import { AsyncComponent } from '@classes/async-component'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -39,7 +40,7 @@ export class CoreComponentsRegistry { | |||||||
|      * @param componentClass Component class. |      * @param componentClass Component class. | ||||||
|      * @returns Component instance. |      * @returns Component instance. | ||||||
|      */ |      */ | ||||||
|     static resolve<T = Component>(element?: Element | null, componentClass?: ComponentConstructor<T>): T | null { |     static resolve<T>(element?: Element | null, componentClass?: ComponentConstructor<T>): T | null { | ||||||
|         const instance = (element && this.instances.get(element) as T) ?? null; |         const instance = (element && this.instances.get(element) as T) ?? null; | ||||||
| 
 | 
 | ||||||
|         return instance && (!componentClass || instance instanceof componentClass) |         return instance && (!componentClass || instance instanceof componentClass) | ||||||
| @ -65,35 +66,45 @@ export class CoreComponentsRegistry { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Waits all elements to be rendered. |      * Get a component instances and wait to be ready. | ||||||
|      * |      * | ||||||
|      * @param element Parent element where to search. |      * @param element Root element. | ||||||
|      * @param selector Selector to search on parent. |      * @param componentClass Component class. | ||||||
|      * @param fnName Component function that have to be resolved when rendered. |  | ||||||
|      * @param params Params of function that have to be resolved when rendered. |  | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     static async finishRenderingAllElementsInside<T = Component>( |     static async waitComponentReady<T extends AsyncComponent>( | ||||||
|         element: Element | undefined | null, |         element: Element | null, | ||||||
|         selector: string, |         componentClass?: ComponentConstructor<T>, | ||||||
|         fnName: string, |  | ||||||
|         params?: unknown[], |  | ||||||
|     ): Promise<void> { |     ): Promise<void> { | ||||||
|         if (!element) { |         const instance = this.resolve(element, componentClass); | ||||||
|  |         if (!instance) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const components = Array |         await instance.ready(); | ||||||
|             .from(element.querySelectorAll(selector)) |     } | ||||||
|             .map(element => CoreComponentsRegistry.resolve<T>(element)); |  | ||||||
| 
 | 
 | ||||||
|         await Promise.all(components.map(component => { |     /** | ||||||
|             if (!component) { |      * Waits all elements matching to be ready. | ||||||
|                 return; |      * | ||||||
|             } |      * @param element Element where to search. | ||||||
| 
 |      * @param selector Selector to search on parent. | ||||||
|             return component[fnName].apply(component, params); |      * @param componentClass Component class. | ||||||
|         })); |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     static async waitComponentsReady<T extends AsyncComponent>( | ||||||
|  |         element: Element, | ||||||
|  |         selector: string, | ||||||
|  |         componentClass?: ComponentConstructor<T>, | ||||||
|  |     ): Promise<void> { | ||||||
|  |         if (element.matches(selector)) { | ||||||
|  |             // Element to wait is myself.
 | ||||||
|  |             await CoreComponentsRegistry.waitComponentReady<T>(element, componentClass); | ||||||
|  |         } else { | ||||||
|  |             await Promise.all(Array | ||||||
|  |                 .from(element.querySelectorAll(selector)) | ||||||
|  |                 .map(element => CoreComponentsRegistry.waitComponentReady<T>(element, componentClass))); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         // Wait for next tick to ensure components are completely rendered.
 |         // Wait for next tick to ensure components are completely rendered.
 | ||||||
|         await CoreUtils.nextTick(); |         await CoreUtils.nextTick(); | ||||||
| @ -105,4 +116,4 @@ export class CoreComponentsRegistry { | |||||||
|  * Component constructor. |  * Component constructor. | ||||||
|  */ |  */ | ||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
| export type ComponentConstructor<T> = { new(...args: any[]): T }; | export type ComponentConstructor<T = Component> = { new(...args: any[]): T }; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user