MOBILE-3814 chore: Improve component registry wait for ready
parent
b1461680a8
commit
4cdc6b9dd6
|
@ -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…
Reference in New Issue