MOBILE-3814 chore: Improve component registry wait for ready

main
Pau Ferrer Ocaña 2022-03-16 13:49:58 +01:00
parent b1461680a8
commit 4cdc6b9dd6
8 changed files with 92 additions and 82 deletions

View 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>;
}

View File

@ -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;
} }
} }

View File

@ -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',
),
]); ]);
} }

View File

@ -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',
);
} }
/** /**

View File

@ -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;

View File

@ -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;
} }

View File

@ -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);
} }
/** /**

View File

@ -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,36 +66,46 @@ 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();
}
/**
* Waits all elements matching to be ready.
*
* @param element Element where to search.
* @param selector Selector to search on parent.
* @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)) .from(element.querySelectorAll(selector))
.map(element => CoreComponentsRegistry.resolve<T>(element)); .map(element => CoreComponentsRegistry.waitComponentReady<T>(element, componentClass)));
await Promise.all(components.map(component => {
if (!component) {
return;
} }
return component[fnName].apply(component, params);
}));
// 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 };