MOBILE-4166 core: Implement CoreDirectivesRegistry and deprecate old one

main
Dani Palou 2023-01-25 10:34:13 +01:00
parent 7341ca28c0
commit 47e5158afe
29 changed files with 392 additions and 178 deletions

View File

@ -47,7 +47,7 @@ import { CanLeave } from '@guards/can-leave';
import { CoreForms } from '@singletons/form';
import { CoreDom } from '@singletons/dom';
import { CoreTime } from '@singletons/time';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
/**
* Page that allows attempting a quiz.
@ -690,7 +690,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
*/
protected async scrollToQuestion(slot: number): Promise<void> {
await CoreUtils.nextTick();
await CoreComponentsRegistry.waitComponentsReady(this.elementRef.nativeElement, 'core-question');
await CoreDirectivesRegistry.waitDirectivesReady(this.elementRef.nativeElement, 'core-question');
await CoreDom.scrollToElement(
this.elementRef.nativeElement,
'#addon-mod_quiz-question-' + slot,

View File

@ -15,7 +15,7 @@
import { CoreFormatTextDirective } from '@directives/format-text';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreCoordinates, CoreDom } from '@singletons/dom';
import { CoreEventObserver } from '@singletons/events';
import { CoreLogger } from '@singletons/logger';
@ -427,7 +427,7 @@ export class AddonQtypeDdwtosQuestion {
protected async waitForReady(): Promise<void> {
await CoreDom.waitToBeInDOM(this.container);
await CoreComponentsRegistry.waitComponentsReady(this.container, 'core-format-text', CoreFormatTextDirective);
await CoreDirectivesRegistry.waitDirectivesReady(this.container, 'core-format-text', CoreFormatTextDirective);
const drag = Array.from(this.container.querySelectorAll<HTMLElement>(this.selectors.dragHomes()))[0];

View File

@ -13,12 +13,12 @@
// limitations under the License.
/**
* Component that is not rendered immediately after being mounted.
* Directive that is not rendered immediately after being mounted.
*/
export interface AsyncComponent {
export interface AsyncDirective {
/**
* Wait until the component is fully rendered and ready.
* Wait until the directive is fully rendered and ready.
*/
ready(): Promise<void>;
}

View File

@ -15,7 +15,7 @@
import { CoreSitesReadingStrategy } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { Subscription } from 'rxjs';
import { AsyncComponent } from './async-component';
import { AsyncDirective } from './async-directive';
import { PageLoadsManager } from './page-loads-manager';
import { CorePromisedValue } from './promised-value';
import { WSObservable } from './site';
@ -27,7 +27,7 @@ export class PageLoadWatcher {
protected hasChanges = false;
protected ongoingRequests = 0;
protected components = new Set<AsyncComponent>();
protected components = new Set<AsyncDirective>();
protected loadedTimeout?: number;
protected hasChangesPromises: Promise<boolean>[] = [];
@ -66,7 +66,7 @@ export class PageLoadWatcher {
*
* @param component Component instance.
*/
async watchComponent(component: AsyncComponent): Promise<void> {
async watchComponent(component: AsyncDirective): Promise<void> {
this.components.add(component);
clearTimeout(this.loadedTimeout);

View File

@ -16,7 +16,7 @@ import { CoreRefreshButtonModalComponent } from '@components/refresh-button-moda
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { Subject } from 'rxjs';
import { AsyncComponent } from './async-component';
import { AsyncDirective } from './async-directive';
import { PageLoadWatcher } from './page-load-watcher';
/**
@ -37,7 +37,7 @@ export class PageLoadsManager {
* @param staleWhileRevalidate Whether to use stale while revalidate strategy.
* @returns Load watcher to use.
*/
startPageLoad(page: AsyncComponent, staleWhileRevalidate: boolean): PageLoadWatcher {
startPageLoad(page: AsyncDirective, staleWhileRevalidate: boolean): PageLoadWatcher {
this.initialPath = this.initialPath ?? CoreNavigator.getCurrentPath();
this.currentLoadWatcher = new PageLoadWatcher(this, staleWhileRevalidate);
this.ongoingLoadWatchers.add(this.currentLoadWatcher);
@ -53,7 +53,7 @@ export class PageLoadsManager {
* @param component Component instance.
* @returns Load watcher to use.
*/
startComponentLoad(component: AsyncComponent): PageLoadWatcher {
startComponentLoad(component: AsyncDirective): PageLoadWatcher {
// If a component is loading data without the page loading data, probably the component is reloading/refreshing.
// In that case, create a load watcher instance but don't store it in currentLoadWatcher because it's not a page load.
const loadWatcher = this.currentLoadWatcher ?? new PageLoadWatcher(this, false);

View File

@ -37,8 +37,8 @@ import { CoreDom } from '@singletons/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreError } from './errors/error';
import { CorePromisedValue } from './promised-value';
import { AsyncComponent } from './async-component';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { AsyncDirective } from './async-directive';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CorePlatform } from '@services/platform';
/**
@ -47,7 +47,7 @@ import { CorePlatform } from '@services/platform';
@Component({
template: '',
})
export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, AfterViewInit, OnChanges, OnDestroy, AsyncComponent {
export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, AfterViewInit, OnChanges, OnDestroy, AsyncDirective {
// Minimum tab's width.
protected static readonly MIN_TAB_WIDTH = 107;
@ -99,7 +99,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
this.tabAction = new CoreTabsRoleTab(this);
CoreComponentsRegistry.register(element.nativeElement, this);
CoreDirectivesRegistry.register(element.nativeElement, this);
}
/**

View File

@ -20,7 +20,7 @@ import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreContextMenuItemComponent } from './context-menu-item';
import { CoreContextMenuPopoverComponent } from './context-menu-popover';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
/**
* This component adds a button (usually in the navigation bar) that displays a context menu popover.
@ -61,7 +61,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy {
// Calculate the unique ID.
this.uniqueId = 'core-context-menu-' + CoreUtils.getUniqueId('CoreContextMenuComponent');
CoreComponentsRegistry.register(elementRef.nativeElement, this);
CoreDirectivesRegistry.register(elementRef.nativeElement, this);
}
/**

View File

@ -18,9 +18,9 @@ import { CoreEventLoadingChangedData, CoreEvents } from '@singletons/events';
import { CoreUtils } from '@services/utils/utils';
import { CoreAnimations } from '@components/animations';
import { Translate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CorePromisedValue } from '@classes/promised-value';
import { AsyncComponent } from '@classes/async-component';
import { AsyncDirective } from '@classes/async-directive';
import { CoreApp } from '@services/app';
/**
@ -49,7 +49,7 @@ import { CoreApp } from '@services/app';
styleUrls: ['loading.scss'],
animations: [CoreAnimations.SHOW_HIDE],
})
export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, AsyncComponent, OnDestroy {
export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, AsyncDirective, OnDestroy {
@Input() hideUntil: unknown = false; // Determine when should the contents be shown.
@Input() message?: string; // Message to show while loading.
@ -65,7 +65,7 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit, A
constructor(element: ElementRef) {
this.element = element.nativeElement;
CoreComponentsRegistry.register(this.element, this);
CoreDirectivesRegistry.register(this.element, this);
// Calculate the unique ID.
this.uniqueId = 'core-loading-content-' + CoreUtils.getUniqueId('CoreLoadingComponent');

View File

@ -25,7 +25,7 @@ import {
import { CoreLogger } from '@singletons/logger';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreContextMenuComponent } from '../context-menu/context-menu';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreDom } from '@singletons/dom';
const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden';
@ -82,7 +82,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
this.element = element.nativeElement;
this.logger = CoreLogger.getInstance('CoreNavBarButtonsComponent');
CoreComponentsRegistry.register(this.element, this);
CoreDirectivesRegistry.register(this.element, this);
}
/**
@ -156,11 +156,11 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
}
const mainContextMenu = buttonsContainer.querySelector('core-context-menu');
const secondaryContextMenuInstance = CoreComponentsRegistry.resolve(secondaryContextMenu, CoreContextMenuComponent);
const secondaryContextMenuInstance = CoreDirectivesRegistry.resolve(secondaryContextMenu, CoreContextMenuComponent);
let mainContextMenuInstance: CoreContextMenuComponent | null;
if (mainContextMenu) {
// Both containers have a context menu. Merge them to prevent having 2 menus at the same time.
mainContextMenuInstance = CoreComponentsRegistry.resolve(mainContextMenu, CoreContextMenuComponent);
mainContextMenuInstance = CoreDirectivesRegistry.resolve(mainContextMenu, CoreContextMenuComponent);
} else {
// There is a context-menu in these buttons, but there is no main context menu in the header.
// Create one main context menu dynamically.

View File

@ -31,7 +31,7 @@ import { CoreNavBarButtonsComponent } from '../navbar-buttons/navbar-buttons';
import { StackEvent } from '@ionic/angular/directives/navigation/stack-utils';
import { CoreNavigator } from '@services/navigator';
import { CoreTabBase, CoreTabsBaseComponent } from '@classes/tabs';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
/**
* This component displays some top scrollable tabs that will autohide on vertical scroll.
@ -207,7 +207,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent<CoreTabsOutle
protected showHideNavBarButtons(activatedPageName: string): void {
const elements = this.ionTabs.outlet.nativeEl.querySelectorAll('core-navbar-buttons');
elements.forEach((element) => {
const instance = CoreComponentsRegistry.resolve(element, CoreNavBarButtonsComponent);
const instance = CoreDirectivesRegistry.resolve(element, CoreNavBarButtonsComponent);
if (instance) {
const pagetagName = element.closest('.ion-page')?.tagName;

View File

@ -16,7 +16,7 @@ import { Component, Input, Output, OnInit, OnDestroy, ElementRef, EventEmitter,
import { CoreTabBase } from '@classes/tabs';
import { CoreUtils } from '@services/utils/utils';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreNavBarButtonsComponent } from '../navbar-buttons/navbar-buttons';
import { CoreTabsComponent } from './tabs';
@ -140,7 +140,7 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase {
protected showHideNavBarButtons(show: boolean): void {
const elements = this.element.querySelectorAll('core-navbar-buttons');
elements.forEach((element) => {
const instance = CoreComponentsRegistry.resolve(element, CoreNavBarButtonsComponent);
const instance = CoreDirectivesRegistry.resolve(element, CoreNavBarButtonsComponent);
if (instance) {
instance.forceHide(!show);

View File

@ -17,7 +17,7 @@ import { ScrollDetail } from '@ionic/core';
import { IonContent } from '@ionic/angular';
import { CoreUtils } from '@services/utils/utils';
import { CoreMath } from '@singletons/math';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreFormatTextDirective } from './format-text';
import { CoreEventObserver } from '@singletons/events';
import { CoreLoadingComponent } from '@components/loading/loading';
@ -203,7 +203,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
* Wait until all <core-format-text> children inside the element are done rendering.
*/
protected async waitFormatTextsRendered(): Promise<void> {
await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective);
await CoreDirectivesRegistry.waitDirectivesReady(this.element, 'core-format-text', CoreFormatTextDirective);
}
/**
@ -249,8 +249,8 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
const scrollElement = await this.ionContent.getScrollElement();
await Promise.all([
await CoreComponentsRegistry.waitComponentsReady(scrollElement, 'core-loading', CoreLoadingComponent),
await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-loading', CoreLoadingComponent),
await CoreDirectivesRegistry.waitDirectivesReady(scrollElement, 'core-loading', CoreLoadingComponent),
await CoreDirectivesRegistry.waitDirectivesReady(this.element, 'core-loading', CoreLoadingComponent),
]);
}

View File

@ -21,7 +21,7 @@ import { CoreTabsComponent } from '@components/tabs/tabs';
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
import { ScrollDetail } from '@ionic/core';
import { CoreUtils } from '@services/utils/utils';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreMath } from '@singletons/math';
@ -294,7 +294,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
this.listenEvents();
// Initialize from tabs.
const tabs = CoreComponentsRegistry.resolve(this.page.querySelector('core-tabs-outlet'), CoreTabsOutletComponent);
const tabs = CoreDirectivesRegistry.resolve(this.page.querySelector('core-tabs-outlet'), CoreTabsOutletComponent);
if (tabs) {
const outlet = tabs.getOutlet();
@ -424,14 +424,14 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
}
// Wait loadings to finish.
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent);
await CoreDirectivesRegistry.waitDirectivesReady(this.page, 'core-loading', CoreLoadingComponent);
// Wait tabs to be ready.
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-tabs', CoreTabsComponent);
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-tabs-outlet', CoreTabsOutletComponent);
await CoreDirectivesRegistry.waitDirectivesReady(this.page, 'core-tabs', CoreTabsComponent);
await CoreDirectivesRegistry.waitDirectivesReady(this.page, 'core-tabs-outlet', CoreTabsOutletComponent);
// Wait loadings to finish, inside tabs (if any).
await CoreComponentsRegistry.waitComponentsReady(
await CoreDirectivesRegistry.waitDirectivesReady(
this.page,
'core-tab core-loading, ion-router-outlet core-loading',
CoreLoadingComponent,
@ -445,7 +445,7 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
* @returns Promise resolved when texts are rendered.
*/
protected async waitFormatTextsRendered(element: Element): Promise<void> {
await CoreComponentsRegistry.waitComponentsReady(element, 'core-format-text', CoreFormatTextDirective);
await CoreDirectivesRegistry.waitDirectivesReady(element, 'core-format-text', CoreFormatTextDirective);
}
/**

View File

@ -19,7 +19,7 @@ import { CoreSettingsHelper } from '@features/settings/services/settings-helper'
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreColors } from '@singletons/colors';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver } from '@singletons/events';
import { Subscription } from 'rxjs';
@ -128,14 +128,14 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy {
return;
}
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent);
await CoreDirectivesRegistry.waitDirectivesReady(this.page, 'core-loading', CoreLoadingComponent);
}
/**
* Wait until all <core-format-text> children inside the element are done rendering.
*/
protected async waitFormatTextsRendered(): Promise<void> {
await CoreComponentsRegistry.waitComponentsReady(this.element, 'core-format-text', CoreFormatTextDirective);
await CoreDirectivesRegistry.waitDirectivesReady(this.element, 'core-format-text', CoreFormatTextDirective);
}
/**

View File

@ -42,10 +42,10 @@ import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from '@feat
import { CoreFilterDelegate } from '@features/filter/services/filter-delegate';
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { CoreSubscriptions } from '@singletons/subscriptions';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreCollapsibleItemDirective } from './collapsible-item';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { AsyncComponent } from '@classes/async-component';
import { AsyncDirective } from '@classes/async-directive';
import { CorePath } from '@singletons/path';
import { CoreDom } from '@singletons/dom';
import { CoreEvents } from '@singletons/events';
@ -67,7 +67,7 @@ import { FrameElementController } from '@classes/element-controllers/FrameElemen
@Directive({
selector: 'core-format-text',
})
export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncComponent {
export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirective {
@ViewChild(CoreCollapsibleItemDirective) collapsible?: CoreCollapsibleItemDirective;
@ -111,7 +111,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
protected viewContainerRef: ViewContainerRef,
@Optional() @Inject(CORE_REFRESH_CONTEXT) protected refreshContext?: CoreRefreshContext,
) {
CoreComponentsRegistry.register(element.nativeElement, this);
CoreDirectivesRegistry.register(element.nativeElement, this);
this.element = element.nativeElement;
this.element.classList.add('core-loading'); // Hide contents until they're treated.

View File

@ -21,7 +21,7 @@ import { CoreCourseBlock } from '../../course/services/course';
import { Params } from '@angular/router';
import { ContextLevel } from '@/core/constants';
import { CoreNavigationOptions } from '@services/navigator';
import { AsyncComponent } from '@classes/async-component';
import { AsyncDirective } from '@classes/async-directive';
import { CorePromisedValue } from '@classes/promised-value';
/**
@ -30,7 +30,7 @@ import { CorePromisedValue } from '@classes/promised-value';
@Component({
template: '',
})
export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockComponent, AsyncComponent {
export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockComponent, AsyncDirective {
@Input() title!: string; // The block title.
@Input() block!: CoreCourseBlock; // The block to render.

View File

@ -79,6 +79,7 @@ import { Md5 } from 'ts-md5/dist/md5';
import { CoreSyncBaseProvider } from '@classes/base-sync';
import { CoreArray } from '@singletons/array';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreDom } from '@singletons/dom';
import { CoreForms } from '@singletons/form';
import { CoreText } from '@singletons/text';
@ -350,6 +351,7 @@ export class CoreCompileProvider {
instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider;
instance['CoreArray'] = CoreArray;
instance['CoreComponentsRegistry'] = CoreComponentsRegistry;
instance['CoreDirectivesRegistry'] = CoreDirectivesRegistry;
instance['CoreNetwork'] = CoreNetwork.instance;
instance['CorePlatform'] = CorePlatform.instance;
instance['CoreDom'] = CoreDom;

View File

@ -14,7 +14,7 @@
import { AddonBlockMyOverviewComponent } from '@addons/block/myoverview/components/myoverview/myoverview';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { AsyncComponent } from '@classes/async-component';
import { AsyncDirective } from '@classes/async-directive';
import { PageLoadsManager } from '@classes/page-loads-manager';
import { CorePromisedValue } from '@classes/promised-value';
import { CoreBlockComponent } from '@features/block/components/block/block';
@ -42,7 +42,7 @@ import { CoreCourses } from '../../services/courses';
useClass: PageLoadsManager,
}],
})
export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy, AsyncComponent {
export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy, AsyncDirective {
@ViewChild(CoreBlockComponent) block!: CoreBlockComponent;

View File

@ -36,7 +36,7 @@ import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventFormActionData, CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreEditorOffline } from '../../services/editor-offline';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreScreen } from '@services/screen';
import { CoreCancellablePromise } from '@classes/cancellable-promise';
@ -304,7 +304,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
return;
}
await CoreComponentsRegistry.waitComponentsReady(page, 'core-loading', CoreLoadingComponent);
await CoreDirectivesRegistry.waitDirectivesReady(page, 'core-loading', CoreLoadingComponent);
}
/**

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Component, Input, Output, OnInit, EventEmitter, ChangeDetectorRef, Type, ElementRef } from '@angular/core';
import { AsyncComponent } from '@classes/async-component';
import { AsyncDirective } from '@classes/async-directive';
import { CorePromisedValue } from '@classes/promised-value';
import { CoreQuestionBehaviourDelegate } from '@features/question/services/behaviour-delegate';
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
@ -22,7 +22,7 @@ import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion }
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreLogger } from '@singletons/logger';
/**
@ -33,7 +33,7 @@ import { CoreLogger } from '@singletons/logger';
templateUrl: 'core-question.html',
styleUrls: ['../../question.scss'],
})
export class CoreQuestionComponent implements OnInit, AsyncComponent {
export class CoreQuestionComponent implements OnInit, AsyncDirective {
@Input() question?: CoreQuestionQuestion; // The question to render.
@Input() component?: string; // The component the question belongs to.
@ -66,7 +66,7 @@ export class CoreQuestionComponent implements OnInit, AsyncComponent {
constructor(protected changeDetector: ChangeDetectorRef, private element: ElementRef) {
this.logger = CoreLogger.getInstance('CoreQuestionComponent');
this.promisedReady = new CorePromisedValue();
CoreComponentsRegistry.register(this.element.nativeElement, this);
CoreDirectivesRegistry.register(this.element.nativeElement, this);
}
async ready(): Promise<void> {

View File

@ -30,7 +30,7 @@ import { CoreUserToursPopoverLayout } from '@features/usertours/classes/popover-
import { CoreUserTours, CoreUserToursAlignment, CoreUserToursSide } from '@features/usertours/services/user-tours';
import { CoreDomUtils } from '@services/utils/dom';
import { AngularFrameworkDelegate } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreDom } from '@singletons/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu';
@ -84,7 +84,7 @@ export class CoreUserToursUserTourComponent implements AfterViewInit, OnDestroy
constructor({ nativeElement: element }: ElementRef<HTMLElement>) {
this.element = element;
CoreComponentsRegistry.register(element, this);
CoreDirectivesRegistry.register(element, this);
this.element.addEventListener('click', (event) =>
this.dismissOnBackOrBackdrop(event.target as HTMLElement));

View File

@ -21,7 +21,7 @@ import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/da
import { CoreApp } from '@services/app';
import { CoreUtils } from '@services/utils/utils';
import { AngularFrameworkDelegate, makeSingleton } from '@singletons';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreDom } from '@singletons/dom';
import { CoreSubscriptions } from '@singletons/subscriptions';
import { CoreUserToursUserTourComponent } from '../components/user-tour/user-tour';
@ -120,7 +120,7 @@ export class CoreUserToursService {
CoreUserToursUserTourComponent,
{ ...componentOptions, container },
);
const tour = CoreComponentsRegistry.require(element, CoreUserToursUserTourComponent);
const tour = CoreDirectivesRegistry.require(element, CoreUserToursUserTourComponent);
return this.startTour(tour, options.watch ?? (options as CoreUserToursFocusedOptions).focus);
}

View File

@ -51,7 +51,7 @@ import { CoreSites } from '@services/sites';
import { NavigationStart } from '@angular/router';
import { filter } from 'rxjs/operators';
import { Subscription } from 'rxjs';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreDom } from '@singletons/dom';
import { CoreNetwork } from '@services/network';
import { CoreSiteError } from '@classes/errors/siteerror';
@ -665,10 +665,10 @@ export class CoreDomUtilsProvider {
*
* @param element The root element of the component/directive.
* @returns The instance, undefined if not found.
* @deprecated since 4.0.0. Use CoreComponentsRegistry instead.
* @deprecated since 4.0.0. Use CoreDirectivesRegistry instead.
*/
getInstanceByElement<T = unknown>(element: Element): T | undefined {
return CoreComponentsRegistry.resolve<T>(element) ?? undefined;
return CoreDirectivesRegistry.resolve<T>(element) ?? undefined;
}
/**
@ -1718,10 +1718,10 @@ export class CoreDomUtilsProvider {
*
* @param element The root element of the component/directive.
* @param instance The instance to store.
* @deprecated since 4.0.0. Use CoreComponentsRegistry instead.
* @deprecated since 4.0.0. Use CoreDirectivesRegistry instead.
*/
storeInstanceByElement(element: Element, instance: unknown): void {
CoreComponentsRegistry.register(element, instance);
CoreDirectivesRegistry.register(element, instance);
}
/**

View File

@ -13,12 +13,14 @@
// limitations under the License.
import { Component } from '@angular/core';
import { AsyncComponent } from '@classes/async-component';
import { AsyncDirective } from '@classes/async-directive';
import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from './logger';
/**
* Registry to keep track of component instances.
*
* @deprecated since 4.1.1. Use CoreDirectivesRegistry instead.
*/
export class CoreComponentsRegistry {
@ -74,7 +76,7 @@ export class CoreComponentsRegistry {
* @param componentClass Component class.
* @returns Promise resolved when done.
*/
static async waitComponentReady<T extends AsyncComponent>(
static async waitComponentReady<T extends AsyncDirective>(
element: Element | null,
componentClass?: ComponentConstructor<T>,
): Promise<void> {
@ -96,7 +98,7 @@ export class CoreComponentsRegistry {
* @param componentClass Component class.
* @returns Promise resolved when done.
*/
static async waitComponentsReady<T extends AsyncComponent>(
static async waitComponentsReady<T extends AsyncDirective>(
element: Element,
selector: string,
componentClass?: ComponentConstructor<T>,

View File

@ -0,0 +1,145 @@
// (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.
import { Directive } from '@angular/core';
import { AsyncDirective } from '@classes/async-directive';
import { CoreUtils } from '@services/utils/utils';
import { CoreLogger } from './logger';
/**
* Registry to keep track of directive instances.
*/
export class CoreDirectivesRegistry {
private static instances: WeakMap<Element, unknown[]> = new WeakMap();
protected static logger = CoreLogger.getInstance('CoreDirectivesRegistry');
/**
* Register a directive instance.
*
* @param element Root element.
* @param instance Directive instance.
*/
static register(element: Element, instance: unknown): void {
const list = this.instances.get(element) ?? [];
list.push(instance);
this.instances.set(element, list);
}
/**
* Resolve a directive instance.
*
* @param element Root element.
* @param directiveClass Directive class.
* @returns Directive instance.
*/
static resolve<T>(element?: Element | null, directiveClass?: DirectiveConstructor<T>): T | null {
const list = (element && this.instances.get(element) as T[]) ?? [];
return list.find(instance => !directiveClass || instance instanceof directiveClass) ?? null;
}
/**
* Resolve all directive instances.
*
* @param element Root element.
* @param directiveClass Directive class.
* @returns Directive instances.
*/
static resolveAll<T>(element?: Element | null, directiveClass?: DirectiveConstructor<T>): T[] {
const list = (element && this.instances.get(element) as T[]) ?? [];
return list.filter(instance => !directiveClass || instance instanceof directiveClass) ?? [];
}
/**
* Get a directive instance and fail if it cannot be resolved.
*
* @param element Root element.
* @param directiveClass Directive class.
* @returns Directive instance.
*/
static require<T>(element: Element, directiveClass?: DirectiveConstructor<T>): T {
const instance = this.resolve(element, directiveClass);
if (!instance) {
throw new Error('Couldn\'t resolve directive instance');
}
return instance;
}
/**
* Get a directive instance and wait to be ready.
*
* @param element Root element.
* @param directiveClass Directive class.
* @returns Promise resolved when done.
*/
static async waitDirectiveReady<T extends AsyncDirective>(
element: Element | null,
directiveClass?: DirectiveConstructor<T>,
): Promise<void> {
const instance = this.resolve(element, directiveClass);
if (!instance) {
this.logger.error('No instance registered for element ' + directiveClass, element);
return;
}
await instance.ready();
}
/**
* Get all directive instances and wait to be ready.
*
* @param element Root element.
* @param directiveClass Directive class.
* @returns Promise resolved when done.
*/
static async waitDirectivesReady<T extends AsyncDirective>(
element: Element,
selector?: string,
directiveClass?: DirectiveConstructor<T>,
): Promise<void> {
let elements: Element[] = [];
if (!selector || element.matches(selector)) {
// Element to wait is myself.
elements = [element];
} else {
elements = Array.from(element.querySelectorAll(selector));
}
if (!elements.length) {
return;
}
await Promise.all(elements.map(async element => {
const instances = this.resolveAll<T>(element, directiveClass);
await Promise.all(instances.map(instance => instance.ready()));
}));
// Wait for next tick to ensure directives are completely rendered.
await CoreUtils.nextTick();
}
}
/**
* Directive constructor.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type DirectiveConstructor<T = Directive> = { new(...args: any[]): T };

View File

@ -108,7 +108,7 @@ export class CoreEvents {
static readonly FILE_SHARED = 'file_shared';
static readonly KEYBOARD_CHANGE = 'keyboard_change';
/**
* @deprecated since app 4.0. Use CoreComponentsRegistry promises instead.
* @deprecated since app 4.0. Use CoreDirectivesRegistry promises instead.
*/
static readonly CORE_LOADING_CHANGED = 'core_loading_changed';
static readonly ORIENTATION_CHANGE = 'orientation_change';

View File

@ -1,104 +0,0 @@
// (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.
import { wait } from '@/testing/utils';
import { CoreComponentsRegistry } from '@singletons/components-registry';
const cssClassName = 'core-components-registry-test';
const createAndRegisterInstance = () => {
const element = document.createElement('div');
element.classList.add(cssClassName);
const instance = new ComponentsRegistryTestClass();
CoreComponentsRegistry.register(element, instance);
return { element, instance };
};
describe('CoreComponentsRegistry singleton', () => {
let element: HTMLElement;
let testClassInstance: ComponentsRegistryTestClass;
beforeEach(() => {
const result = createAndRegisterInstance();
element = result.element;
testClassInstance = result.instance;
});
it('resolves stored instances', () => {
expect(CoreComponentsRegistry.resolve(element)).toEqual(testClassInstance);
expect(CoreComponentsRegistry.resolve(element, ComponentsRegistryTestClass)).toEqual(testClassInstance);
expect(CoreComponentsRegistry.resolve(element, CoreComponentsRegistry)).toEqual(null);
expect(CoreComponentsRegistry.resolve(document.createElement('div'))).toEqual(null);
});
it('requires stored instances', () => {
expect(CoreComponentsRegistry.require(element)).toEqual(testClassInstance);
expect(CoreComponentsRegistry.require(element, ComponentsRegistryTestClass)).toEqual(testClassInstance);
expect(() => CoreComponentsRegistry.require(element, CoreComponentsRegistry)).toThrow();
expect(() => CoreComponentsRegistry.require(document.createElement('div'))).toThrow();
});
it('waits for component ready', async () => {
expect(testClassInstance.isReady).toBe(false);
await CoreComponentsRegistry.waitComponentReady(element);
expect(testClassInstance.isReady).toBe(true);
});
it('waits for components ready: just one', async () => {
expect(testClassInstance.isReady).toBe(false);
await CoreComponentsRegistry.waitComponentsReady(element, `.${cssClassName}`);
expect(testClassInstance.isReady).toBe(true);
});
it('waits for components ready: multiple', async () => {
const secondResult = createAndRegisterInstance();
const thirdResult = createAndRegisterInstance();
thirdResult.element.classList.remove(cssClassName); // Remove the class so the element and instance aren't treated.
const parent = document.createElement('div');
parent.appendChild(element);
parent.appendChild(secondResult.element);
parent.appendChild(thirdResult.element);
expect(testClassInstance.isReady).toBe(false);
expect(secondResult.instance.isReady).toBe(false);
expect(thirdResult.instance.isReady).toBe(false);
await CoreComponentsRegistry.waitComponentsReady(parent, `.${cssClassName}`);
expect(testClassInstance.isReady).toBe(true);
expect(secondResult.instance.isReady).toBe(true);
expect(thirdResult.instance.isReady).toBe(false);
});
});
class ComponentsRegistryTestClass {
randomId = Math.random();
isReady = false;
async ready(): Promise<void> {
await wait(50);
this.isReady = true;
}
}

View File

@ -0,0 +1,169 @@
// (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.
import { wait } from '@/testing/utils';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
const cssClassName = 'core-directives-registry-test';
const createAndRegisterInstance = (element?: HTMLElement) => {
element = element ?? document.createElement('div');
element.classList.add(cssClassName);
const instance = new DirectivesRegistryTestClass();
CoreDirectivesRegistry.register(element, instance);
return { element, instance };
};
describe('CoreDirectivesRegistry singleton', () => {
let element: HTMLElement;
let testClassInstance: DirectivesRegistryTestClass;
let testClassSecondInstance: DirectivesRegistryTestClass;
let testAltClassInstance: DirectivesRegistryAltTestClass;
beforeEach(() => {
let result = createAndRegisterInstance();
element = result.element;
testClassInstance = result.instance;
result = createAndRegisterInstance(element);
testClassSecondInstance = result.instance;
testAltClassInstance = new DirectivesRegistryAltTestClass();
CoreDirectivesRegistry.register(element, testAltClassInstance);
});
it('resolves a stored instance', () => {
expect(CoreDirectivesRegistry.resolve(element)).toEqual(testClassInstance);
expect(CoreDirectivesRegistry.resolve(element, DirectivesRegistryTestClass)).toEqual(testClassInstance);
expect(CoreDirectivesRegistry.resolve(element, DirectivesRegistryAltTestClass)).toEqual(testAltClassInstance);
expect(CoreDirectivesRegistry.resolve(element, CoreDirectivesRegistry)).toEqual(null);
expect(CoreDirectivesRegistry.resolve(document.createElement('div'))).toEqual(null);
});
it('resolves all stored instances', () => {
expect(CoreDirectivesRegistry.resolveAll(element)).toEqual(
[testClassInstance, testClassSecondInstance, testAltClassInstance],
);
expect(CoreDirectivesRegistry.resolveAll(element, DirectivesRegistryTestClass)).toEqual(
[testClassInstance, testClassSecondInstance],
);
expect(CoreDirectivesRegistry.resolveAll(element, DirectivesRegistryAltTestClass)).toEqual([testAltClassInstance]);
expect(CoreDirectivesRegistry.resolveAll(element, CoreDirectivesRegistry)).toEqual([]);
expect(CoreDirectivesRegistry.resolveAll(document.createElement('div'))).toEqual([]);
});
it('requires a stored instance', () => {
expect(CoreDirectivesRegistry.require(element)).toEqual(testClassInstance);
expect(CoreDirectivesRegistry.require(element, DirectivesRegistryTestClass)).toEqual(testClassInstance);
expect(CoreDirectivesRegistry.require(element, DirectivesRegistryAltTestClass)).toEqual(testAltClassInstance);
expect(() => CoreDirectivesRegistry.require(element, CoreDirectivesRegistry)).toThrow();
expect(() => CoreDirectivesRegistry.require(document.createElement('div'))).toThrow();
});
it('waits for directive ready', async () => {
expect(testClassInstance.isReady).toBe(false);
await CoreDirectivesRegistry.waitDirectiveReady(element);
expect(testClassInstance.isReady).toBe(true);
});
it('waits for directives ready: just one element and directive', async () => {
const result = createAndRegisterInstance();
expect(result.instance.isReady).toBe(false);
await CoreDirectivesRegistry.waitDirectivesReady(result.element, `.${cssClassName}`);
expect(result.instance.isReady).toBe(true);
expect(testClassInstance.isReady).toBe(false);
});
it('waits for directives ready: all directives, single element', async () => {
expect(testClassInstance.isReady).toBe(false);
expect(testClassSecondInstance.isReady).toBe(false);
expect(testAltClassInstance.isReady).toBe(false);
await CoreDirectivesRegistry.waitDirectivesReady(element);
expect(testClassInstance.isReady).toBe(true);
expect(testClassSecondInstance.isReady).toBe(true);
expect(testAltClassInstance.isReady).toBe(true);
});
it('waits for directives ready: filter by class, single element', async () => {
expect(testClassInstance.isReady).toBe(false);
expect(testClassSecondInstance.isReady).toBe(false);
expect(testAltClassInstance.isReady).toBe(false);
await CoreDirectivesRegistry.waitDirectivesReady(element, `.${cssClassName}`, DirectivesRegistryTestClass);
expect(testClassInstance.isReady).toBe(true);
expect(testClassSecondInstance.isReady).toBe(true);
expect(testAltClassInstance.isReady).toBe(false);
});
it('waits for directives ready: multiple elements', async () => {
const secondResult = createAndRegisterInstance();
const thirdResult = createAndRegisterInstance();
thirdResult.element.classList.remove(cssClassName); // Remove the class so the element and instance aren't treated.
const parent = document.createElement('div');
parent.appendChild(element);
parent.appendChild(secondResult.element);
parent.appendChild(thirdResult.element);
expect(testClassInstance.isReady).toBe(false);
expect(testClassSecondInstance.isReady).toBe(false);
expect(testAltClassInstance.isReady).toBe(false);
expect(secondResult.instance.isReady).toBe(false);
expect(thirdResult.instance.isReady).toBe(false);
await CoreDirectivesRegistry.waitDirectivesReady(parent, `.${cssClassName}`, DirectivesRegistryTestClass);
expect(testClassInstance.isReady).toBe(true);
expect(testClassSecondInstance.isReady).toBe(true);
expect(testAltClassInstance.isReady).toBe(false);
expect(secondResult.instance.isReady).toBe(true);
expect(thirdResult.instance.isReady).toBe(false);
});
});
class DirectivesRegistryTestClass {
randomId = Math.random();
isReady = false;
async ready(): Promise<void> {
await wait(50);
this.isReady = true;
}
}
class DirectivesRegistryAltTestClass {
randomId = Math.random();
isReady = false;
async ready(): Promise<void> {
await wait(50);
this.isReady = true;
}
}

View File

@ -23,7 +23,7 @@ import { CoreNetwork, CoreNetworkService } from '@services/network';
import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications';
import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron';
import { CoreLoadingComponent } from '@components/loading/loading';
import { CoreComponentsRegistry } from '@singletons/components-registry';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreDom } from '@singletons/dom';
import { Injectable } from '@angular/core';
import { CoreSites, CoreSitesProvider } from '@services/sites';
@ -127,7 +127,7 @@ export class TestingBehatRuntimeService {
.filter((element) => CoreDom.isElementVisible(element));
await Promise.all(elements.map(element =>
CoreComponentsRegistry.waitComponentReady(element, CoreLoadingComponent)));
CoreDirectivesRegistry.waitDirectiveReady(element, CoreLoadingComponent)));
});
}