From 4cdc6b9dd6934bf5aa5fcf2d79e2cd4fefc3e977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 16 Mar 2022 13:49:58 +0100 Subject: [PATCH] MOBILE-3814 chore: Improve component registry wait for ready --- src/core/classes/async-component.ts | 24 ++++++++ src/core/components/loading/loading.ts | 15 +++-- src/core/directives/collapsible-footer.ts | 21 ++----- src/core/directives/collapsible-header.ts | 18 ++---- src/core/directives/collapsible-item.ts | 27 +++------ src/core/directives/format-text.ts | 7 ++- .../rich-text-editor/rich-text-editor.ts | 5 +- src/core/singletons/components-registry.ts | 57 +++++++++++-------- 8 files changed, 92 insertions(+), 82 deletions(-) create mode 100644 src/core/classes/async-component.ts diff --git a/src/core/classes/async-component.ts b/src/core/classes/async-component.ts new file mode 100644 index 000000000..77635a70f --- /dev/null +++ b/src/core/classes/async-component.ts @@ -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; +} diff --git a/src/core/components/loading/loading.ts b/src/core/components/loading/loading.ts index 2fbd213cc..d5be19770 100644 --- a/src/core/components/loading/loading.ts +++ b/src/core/components/loading/loading.ts @@ -20,6 +20,7 @@ import { CoreAnimations } from '@components/animations'; import { Translate } from '@singletons'; import { CoreComponentsRegistry } from '@singletons/components-registry'; 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. @@ -47,7 +48,7 @@ import { CorePromisedValue } from '@classes/promised-value'; styleUrls: ['loading.scss'], 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() message?: string; // Message to show while loading. @@ -58,7 +59,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { uniqueId: string; protected element: HTMLElement; // Current element. loaded = false; // Only comes true once. - protected firstLoadedPromise = new CorePromisedValue(); + protected onReadyPromise = new CorePromisedValue(); constructor(element: ElementRef) { this.element = element.nativeElement; @@ -108,7 +109,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { if (!this.loaded && loaded) { this.loaded = true; // Only comes true once. - this.firstLoadedPromise.resolve(this.uniqueId); + this.onReadyPromise.resolve(); } // Event has been deprecated since app 4.0. @@ -119,12 +120,10 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { } /** - * Wait the loading to finish. - * - * @return Promise resolved with the uniqueId when done. + * @inheritdoc */ - async whenLoaded(): Promise { - return await this.firstLoadedPromise; + async ready(): Promise { + return await this.onReadyPromise; } } diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index f63d1f57c..2dccfd760 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -66,7 +66,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { await this.domPromise; await this.waitLoadingsDone(); - await this.waitFormatTextsRendered(this.element); + await this.waitFormatTextsRendered(); this.content = this.element.closest('ion-content'); @@ -156,15 +156,9 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { /** * Wait until all children inside the element are done rendering. - * - * @param element Element. */ - protected async waitFormatTextsRendered(element: Element): Promise { - await CoreComponentsRegistry.finishRenderingAllElementsInside( - element, - 'core-format-text', - 'rendered', - ); + protected async waitFormatTextsRendered(): Promise { + await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective); } /** @@ -210,13 +204,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { const scrollElement = await this.ionContent.getScrollElement(); await Promise.all([ - await CoreComponentsRegistry.finishRenderingAllElementsInside - (scrollElement, 'core-loading', 'whenLoaded'), - await CoreComponentsRegistry.finishRenderingAllElementsInside( - this.element, - 'core-loading', - 'whenLoaded', - ), + await CoreComponentsRegistry.waitComponentsReady(scrollElement, 'core-loading', CoreLoadingComponent), + await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-loading', CoreLoadingComponent), ]); } diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts index 69f4929ef..0bf4d0738 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -332,15 +332,13 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest /** * Wait until all children inside the page. - * - * @return Promise resolved when loadings are done. */ protected async waitLoadingsDone(): Promise { - await CoreComponentsRegistry.finishRenderingAllElementsInside( - this.page, - 'core-loading', - 'whenLoaded', - ); + if (!this.page) { + return; + } + + 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. */ protected async waitFormatTextsRendered(element: Element): Promise { - await CoreComponentsRegistry.finishRenderingAllElementsInside( - element, - 'core-format-text', - 'rendered', - ); + await CoreComponentsRegistry.waitComponentsReady(element, 'core-format-text', CoreFormatTextDirective); } /** diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts index 08e987035..41990f722 100644 --- a/src/core/directives/collapsible-item.ts +++ b/src/core/directives/collapsible-item.ts @@ -16,7 +16,6 @@ import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreEventObserver } from '@singletons/events'; @@ -103,28 +102,18 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { const page = this.element.closest('.ion-page'); - await CoreComponentsRegistry.finishRenderingAllElementsInside(page, 'core-loading', 'whenLoaded'); + if (!page) { + return; + } + + await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent); } /** * Wait until all children inside the element are done rendering. - * - * @param element Element. */ - protected async waitFormatTextsRendered(element: Element): Promise { - let formatTextElements: HTMLElement[] = []; - - 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(); + protected async waitFormatTextsRendered(): Promise { + await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective); } /** @@ -134,7 +123,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { // Remove max-height (if any) to calculate the real height. this.element.classList.add('collapsible-loading-height'); - await this.waitFormatTextsRendered(this.element); + await this.waitFormatTextsRendered(); this.expandedHeight = this.element.getBoundingClientRect().height; diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 42c780f76..afd53beae 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -43,6 +43,7 @@ import { CoreSubscriptions } from '@singletons/subscriptions'; import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreCollapsibleItemDirective } from './collapsible-item'; 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 @@ -56,7 +57,7 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise'; @Directive({ selector: 'core-format-text', }) -export class CoreFormatTextDirective implements OnChanges, OnDestroy { +export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncComponent { @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 { + async ready(): Promise { if (!this.element.classList.contains('core-format-text-loading')) { return; } diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index 550682a86..18a9760fb 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -294,8 +294,11 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn await this.domPromise; const page = this.element.closest('.ion-page'); + if (!page) { + return; + } - await CoreComponentsRegistry.finishRenderingAllElementsInside(page, 'core-loading', 'whenLoaded'); + await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent); } /** diff --git a/src/core/singletons/components-registry.ts b/src/core/singletons/components-registry.ts index 5198ee90b..32b08e3dd 100644 --- a/src/core/singletons/components-registry.ts +++ b/src/core/singletons/components-registry.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Component } from '@angular/core'; +import { AsyncComponent } from '@classes/async-component'; import { CoreUtils } from '@services/utils/utils'; /** @@ -39,7 +40,7 @@ export class CoreComponentsRegistry { * @param componentClass Component class. * @returns Component instance. */ - static resolve(element?: Element | null, componentClass?: ComponentConstructor): T | null { + static resolve(element?: Element | null, componentClass?: ComponentConstructor): T | null { const instance = (element && this.instances.get(element) as T) ?? null; 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 selector Selector to search on parent. - * @param fnName Component function that have to be resolved when rendered. - * @param params Params of function that have to be resolved when rendered. + * @param element Root element. + * @param componentClass Component class. * @return Promise resolved when done. */ - static async finishRenderingAllElementsInside( - element: Element | undefined | null, - selector: string, - fnName: string, - params?: unknown[], + static async waitComponentReady( + element: Element | null, + componentClass?: ComponentConstructor, ): Promise { - if (!element) { + const instance = this.resolve(element, componentClass); + if (!instance) { return; } - const components = Array - .from(element.querySelectorAll(selector)) - .map(element => CoreComponentsRegistry.resolve(element)); + await instance.ready(); + } - await Promise.all(components.map(component => { - if (!component) { - return; - } - - return component[fnName].apply(component, params); - })); + /** + * 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( + element: Element, + selector: string, + componentClass?: ComponentConstructor, + ): Promise { + if (element.matches(selector)) { + // Element to wait is myself. + await CoreComponentsRegistry.waitComponentReady(element, componentClass); + } else { + await Promise.all(Array + .from(element.querySelectorAll(selector)) + .map(element => CoreComponentsRegistry.waitComponentReady(element, componentClass))); + } // Wait for next tick to ensure components are completely rendered. await CoreUtils.nextTick(); @@ -105,4 +116,4 @@ export class CoreComponentsRegistry { * Component constructor. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ComponentConstructor = { new(...args: any[]): T }; +export type ComponentConstructor = { new(...args: any[]): T };