MOBILE-3814 collapsible: Wait to dom and loading with component registry

main
Pau Ferrer Ocaña 2022-03-11 15:58:02 +01:00
parent 81e227a8fa
commit f38372642d
5 changed files with 107 additions and 47 deletions

View File

@ -19,8 +19,8 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreMath } from '@singletons/math';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreFormatTextDirective } from './format-text';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreEventObserver } from '@singletons/events';
import { CoreLoadingComponent } from '@components/loading/loading';
/**
* Directive to make an element fixed at the bottom collapsible when scrolling.
@ -139,11 +139,11 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
* @param element Element.
*/
protected async waitFormatTextsRendered(element: Element): Promise<void> {
const formatTexts = Array
.from(element.querySelectorAll('core-format-text'))
.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective));
await Promise.all(formatTexts.map(formatText => formatText?.rendered()));
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>(
element,
'core-format-text',
'rendered',
);
}
/**
@ -184,24 +184,33 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
// Calculate the height now.
await this.calculateHeight();
setTimeout(() => this.calculateHeight(), 200); // Try again, sometimes the first calculation is wrong.
this.listenScrollEvents();
// Only if not present or explicitly falsy it will be false.
this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom);
// Recalculate the height if a parent core-loading displays the content.
this.loadingChangedListener =
CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data: CoreEventLoadingChangedData) => {
if (data.loaded && CoreDomUtils.closest(this.element.parentElement, '#' + data.uniqueId)) {
// The format-text is inside the loading, re-calculate the height.
await this.waitLoadingsDone();
await this.calculateHeight();
setTimeout(() => this.calculateHeight(), 200);
this.listenScrollEvents();
}
});
/**
* Wait until all <core-loading> children inside the page.
*
* @return Promise resolved when loadings are done.
*/
protected async waitLoadingsDone(): Promise<void> {
const scrollElement = await this.ionContent.getScrollElement();
await Promise.all([
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>
(scrollElement, 'core-loading', 'whenLoaded'),
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(
this.element,
'core-loading',
'whenLoaded',
),
]);
}
/**

View File

@ -284,16 +284,25 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
* @return Promise resolved when loadings are done.
*/
protected async waitLoadingsDone(): Promise<void> {
await CoreComponentsRegistry.resolveAllElementsInside<CoreLoadingComponent>(this.page, 'core-loading', 'whenLoaded');
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(
this.page,
'core-loading',
'whenLoaded',
);
}
/**
* Wait until all <core-format-text> children inside the element are done rendering.
*
* @param element Element.
* @return Promise resolved when texts are rendered.
*/
protected async waitFormatTextsRendered(element: Element): Promise<void> {
await CoreComponentsRegistry.resolveAllElementsInside<CoreFormatTextDirective>(element, 'core-format-text', 'rendered');
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>(
element,
'core-format-text',
'rendered',
);
}
/**

View File

@ -13,11 +13,11 @@
// limitations under the License.
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
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 { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreFormatTextDirective } from './format-text';
const defaultMaxHeight = 80;
@ -47,7 +47,6 @@ export class CoreCollapsibleItemDirective implements OnInit {
protected expanded = false;
protected maxHeight = defaultMaxHeight;
protected expandedHeight = 0;
protected loadingChangedListener?: CoreEventObserver;
constructor(el: ElementRef<HTMLElement>) {
this.element = el.nativeElement;
@ -79,17 +78,22 @@ export class CoreCollapsibleItemDirective implements OnInit {
this.element.classList.add('collapsible-item');
// Calculate the height now.
await this.calculateHeight();
await this.waitLoadingsDone();
// Recalculate the height if a parent core-loading displays the content.
this.loadingChangedListener =
CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data: CoreEventLoadingChangedData) => {
if (data.loaded && CoreDomUtils.closest(this.element.parentElement, '#' + data.uniqueId)) {
// The element is inside the loading, re-calculate the height.
await this.calculateHeight();
}
});
/**
* Wait until all <core-loading> children inside the page.
*
* @return Promise resolved when loadings are done.
*/
protected async waitLoadingsDone(): Promise<void> {
await CoreDomUtils.waitToDom(this.element);
const page = this.element.closest('.ion-page');
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(page, 'core-loading', 'whenLoaded');
}
/**
@ -109,19 +113,19 @@ export class CoreCollapsibleItemDirective implements OnInit {
const formatTexts = formatTextElements.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective));
await Promise.all(formatTexts.map(formatText => formatText?.rendered()));
await CoreUtils.nextTick();
}
/**
* Calculate the height and check if we need to display show more or not.
*/
protected async calculateHeight(retries = 3): Promise<void> {
protected async calculateHeight(): Promise<void> {
// Remove max-height (if any) to calculate the real height.
this.element.classList.add('collapsible-loading-height');
await this.waitFormatTextsRendered(this.element);
await CoreUtils.nextTick();
this.expandedHeight = CoreDomUtils.getElementHeight(this.element) || 0;
// Restore the max height now.
@ -129,10 +133,6 @@ export class CoreCollapsibleItemDirective implements OnInit {
// If cannot calculate height, shorten always.
this.setExpandButtonEnabled(!this.expandedHeight || this.expandedHeight >= this.maxHeight);
if (this.expandedHeight == 0 && retries > 0) {
setTimeout(() => this.calculateHeight(retries - 1), 200);
}
}
/**

View File

@ -53,6 +53,7 @@ import { NavigationStart } from '@angular/router';
import { filter } from 'rxjs/operators';
import { Subscription } from 'rxjs';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CorePromisedValue } from '@classes/promised-value';
/*
* "Utils" service with helper functions for UI, DOM elements and HTML code.
@ -91,6 +92,45 @@ export class CoreDomUtilsProvider {
this.debugDisplay = debugDisplay != 0;
}
/**
* Wait an element to be in dom of another element.
*
* @param element Element to wait.
* @return Promise resolved when added. It will be rejected after a timeout of 5s.
*/
waitToDom(
element: Element,
): CorePromisedValue<void> {
let root = element.getRootNode({ composed: true });
const inDomPromise = new CorePromisedValue<void>();
if (root === document) {
// Already in DOM.
inDomPromise.resolve();
return inDomPromise;
}
// Disconnect observer for performance reasons.
const timeout = window.setTimeout(() => {
inDomPromise.reject(new Error('Waiting for DOM timeout reached'));
observer.disconnect();
}, 5000);
const observer = new MutationObserver(() => {
root = element.getRootNode({ composed: true });
if (root === document) {
observer.disconnect();
clearTimeout(timeout);
inDomPromise.resolve();
}
});
observer.observe(document.body, { subtree: true, childList: true });
return inDomPromise;
}
/**
* Equivalent to element.closest(). If the browser doesn't support element.closest, it will
* traverse the parents to achieve the same functionality.

View File

@ -48,15 +48,15 @@ export class CoreComponentsRegistry {
}
/**
* Waits all elements to be resolved.
* Waits all elements to be rendered.
*
* @param element Parent element where to search.
* @param selector Selector to search on parent.
* @param fnName Function that needs to get resolved.
* @param params Params of function that needs to get resolved.
* @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.
*/
static async resolveAllElementsInside<T = Component>(
static async finishRenderingAllElementsInside<T = Component>(
element: Element | undefined | null,
selector: string,
fnName: string,
@ -77,6 +77,8 @@ export class CoreComponentsRegistry {
return component[fnName].apply(component, params);
}));
// Wait for next tick to ensure components are completely rendered.
await CoreUtils.nextTick();
}