Extract list items management from settings

main
Noel De Martin 2021-02-04 19:55:26 +01:00
parent 6f54e0eb06
commit 98910cf465
5 changed files with 283 additions and 70 deletions

View File

@ -0,0 +1,202 @@
// (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 { ActivatedRouteSnapshot, Params } from '@angular/router';
import { Subscription } from 'rxjs';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreNavigator } from '@services/navigator';
import { CoreScreen } from '@services/screen';
import { CoreUtils } from '@services/utils/utils';
/**
* Helper class to manage the state and routing of a list of items in a page, for example on pages using a split view.
*/
export abstract class CorePageItemsListManager<Item> {
protected itemsList: Item[] | null = null;
protected itemsMap: Record<string, Item> | null = null;
protected selectedItem: Item | null = null;
protected pageComponent: unknown;
protected splitView?: CoreSplitViewComponent;
protected splitViewOutletSubscription?: Subscription;
constructor(pageComponent: unknown) {
this.pageComponent = pageComponent;
}
get items(): Item[] {
return this.itemsList || [];
}
get loaded(): boolean {
return this.itemsMap !== null;
}
get empty(): boolean {
return this.itemsList === null || this.itemsList.length === 0;
}
/**
* Process page started operations.
*/
async start(): Promise<void> {
// Calculate current selected item.
const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent });
if (route !== null && route.firstChild) {
this.updateSelectedItem(route.firstChild.snapshot);
}
// Select default item if none is selected on a non-mobile layout.
if (!CoreScreen.instance.isMobile && this.selectedItem === null) {
const defaultItem = this.getDefaultItem();
if (defaultItem) {
this.select(defaultItem);
}
}
// Log activity.
await CoreUtils.instance.ignoreErrors(this.logActivity());
}
/**
* Process page destroyed operations.
*/
destroy(): void {
this.splitViewOutletSubscription?.unsubscribe();
}
/**
* Watch a split view outlet to keep track of the selected item.
*
* @param splitView Split view component.
*/
watchSplitViewOutlet(splitView: CoreSplitViewComponent): void {
this.splitView = splitView;
this.splitViewOutletSubscription = splitView.outletRouteObservable.subscribe(route => this.updateSelectedItem(route));
this.updateSelectedItem(splitView.outletRoute);
}
// @todo Implement watchResize.
/**
* Check whether the given item is selected or not.
*
* @param item Item.
* @return Whether the given item is selected.
*/
isSelected(item: Item): boolean {
return this.selectedItem === item;
}
/**
* Select an item.
*
* @param item Item.
*/
async select(item: Item): Promise<void> {
// Get current route in the page.
const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent });
if (route === null) {
return;
}
// If this item is already selected, do nothing.
const itemPath = this.getItemPath(item);
if (route.firstChild?.routeConfig?.path === itemPath) {
return;
}
// Navigate to item.
const path = route.firstChild ? `../${itemPath}` : itemPath;
const params = this.getItemQueryParams(item);
await CoreNavigator.instance.navigate(path, { params });
}
/**
* Set the list of items.
*
* @param items Items.
*/
setItems(items: Item[]): void {
this.itemsList = items.slice(0);
this.itemsMap = items.reduce((map, item) => {
map[this.getItemPath(item)] = item;
return map;
}, {});
this.updateSelectedItem(this.splitView?.outletRoute);
}
/**
* Log activity when the page starts.
*/
protected async logActivity(): Promise<void> {
//
}
/**
* Update the selected item given the current route.
*
* @param route Current route.
*/
protected updateSelectedItem(route?: ActivatedRouteSnapshot | null): void {
const selectedItemPath = route ? this.getSelectedItemPath(route) : null;
this.selectedItem = selectedItemPath
? this.itemsMap?.[selectedItemPath] ?? null
: null;
}
/**
* Get the item that should be selected by default.
*/
protected getDefaultItem(): Item | null {
return this.itemsList?.[0] || null;
}
/**
* Get the query parameters to use when navigating to an item page.
*
* @param item Item.
* @return Query parameters to use when navigating to the item page.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getItemQueryParams(item: Item): Params {
return {};
}
/**
* Get the path to use when navigating to an item page.
*
* @param item Item.
* @return Path to use when navigating to the item page.
*/
protected abstract getItemPath(item: Item): string;
/**
* Get the path of the selected item given the current route.
*
* @param route Current route.
* @return Path of the selected item in the given route.
*/
protected abstract getSelectedItemPath(route: ActivatedRouteSnapshot): string | null;
}

View File

@ -13,9 +13,10 @@
// limitations under the License.
import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { IonRouterOutlet } from '@ionic/angular';
import { CoreScreen } from '@services/screen';
import { Subscription } from 'rxjs';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
enum CoreSplitViewMode {
MenuOnly = 'menu-only', // Hides content.
@ -35,18 +36,33 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
@Input() placeholderText = 'core.emptysplit';
isNested = false;
private outletRouteSubject: BehaviorSubject<ActivatedRouteSnapshot | null> = new BehaviorSubject(null);
private subscriptions?: Subscription[];
constructor(private element: ElementRef<HTMLElement>) {}
get outletRoute(): ActivatedRouteSnapshot | null {
return this.outletRouteSubject.value;
}
get outletRouteObservable(): Observable<ActivatedRouteSnapshot | null> {
return this.outletRouteSubject.asObservable();
}
/**
* @inheritdoc
*/
ngAfterViewInit(): void {
this.isNested = !!this.element.nativeElement.parentElement?.closest('core-split-view');
this.subscriptions = [
this.outlet.activateEvents.subscribe(() => this.updateClasses()),
this.outlet.deactivateEvents.subscribe(() => this.updateClasses()),
this.outlet.activateEvents.subscribe(() => {
this.updateClasses();
this.outletRouteSubject.next(this.outlet.activatedRoute.snapshot);
}),
this.outlet.deactivateEvents.subscribe(() => {
this.updateClasses();
this.outletRouteSubject.next(null);
}),
CoreScreen.instance.layoutObservable.subscribe(() => this.updateClasses()),
];

View File

@ -11,11 +11,11 @@
<core-split-view>
<ion-list>
<ion-item
*ngFor="let section of sections"
[class.core-selected-item]="section.name === activeSection"
*ngFor="let section of sections.items"
[class.core-selected-item]="sections.isSelected(section)"
button
detail
(click)="openSection(section)"
(click)="sections.select(section)"
>
<ion-icon [name]="section.icon" slot="start"></ion-icon>
<ion-label>{{ 'core.settings.' + section.name | translate }}</ion-label>

View File

@ -12,83 +12,57 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { CoreNavigator } from '@services/navigator';
import { CoreScreen } from '@services/screen';
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { CoreSettingsConstants, CoreSettingsSection } from '@features/settings/constants';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { ActivatedRouteSnapshot } from '@angular/router';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
@Component({
selector: 'page-core-settings-index',
templateUrl: 'index.html',
})
export class CoreSettingsIndexPage implements OnInit, OnDestroy {
export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy {
sections = CoreSettingsConstants.SECTIONS;
activeSection?: string;
layoutSubscription?: Subscription;
sections: CoreSettingsSectionsManager = new CoreSettingsSectionsManager(CoreSettingsIndexPage);
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
/**
* @inheritdoc
*/
ngOnInit(): void {
this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveSection());
}
/**
* @inheritdoc
*/
ionViewWillEnter(): void {
this.updateActiveSection();
ngAfterViewInit(): void {
this.sections.setItems(CoreSettingsConstants.SECTIONS);
this.sections.watchSplitViewOutlet(this.splitView);
this.sections.start();
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.layoutSubscription?.unsubscribe();
this.sections.destroy();
}
}
/**
* Open a section page.
*
* @param section Section to open.
* Helper class to manage sections.
*/
openSection(section: CoreSettingsSection): void {
const path = this.activeSection ? `../${section.path}` : section.path;
class CoreSettingsSectionsManager extends CorePageItemsListManager<CoreSettingsSection> {
CoreNavigator.instance.navigate(path);
this.updateActiveSection(section.name);
/**
* @inheritdoc
*/
protected getItemPath(section: CoreSettingsSection): string {
return section.path;
}
/**
* Update active section.
*
* @param activeSection Active section.
* @inheritdoc
*/
private updateActiveSection(activeSection?: string): void {
if (CoreScreen.instance.isMobile) {
delete this.activeSection;
return;
}
this.activeSection = activeSection ?? this.guessActiveSection();
}
/**
* Guess active section looking at the current route.
*
* @return Active section.
*/
private guessActiveSection(): string | undefined {
const activeSection = this.sections.find(
section => CoreNavigator.instance.isCurrent(`**/settings/${section.path}`),
);
return activeSection?.name;
protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null {
return route.parent?.routeConfig?.path ?? null;
}
}

View File

@ -50,6 +50,14 @@ export type CoreNavigationOptions = {
reset?: boolean;
};
/**
* Options for CoreNavigatorService#getCurrentRoute method.
*/
type GetCurrentRouteOptions = Partial<{
parentRoute: ActivatedRoute;
pageComponent: unknown;
}>;
/**
* Service to provide some helper functions regarding navigation.
*/
@ -310,13 +318,26 @@ export class CoreNavigatorService {
/**
* Get current activated route.
*
* @param route Parent route.
* @param options
* - parent: Parent route, if this isn't provided the current active route will be used.
* - pageComponent: Page component of the route to find, if this isn't provided the deepest route in the hierarchy
* will be returned.
* @return Current activated route.
*/
protected getCurrentRoute(route?: ActivatedRoute): ActivatedRoute {
route = route ?? Router.instance.routerState.root;
getCurrentRoute(): ActivatedRoute;
getCurrentRoute(options: GetCurrentRouteOptions): ActivatedRoute | null;
getCurrentRoute({ parentRoute, pageComponent }: GetCurrentRouteOptions = {}): ActivatedRoute | null {
parentRoute = parentRoute ?? Router.instance.routerState.root;
return route.firstChild ? this.getCurrentRoute(route.firstChild) : route;
if (pageComponent && parentRoute.component === pageComponent) {
return parentRoute;
}
if (parentRoute.firstChild) {
return this.getCurrentRoute({ parentRoute: parentRoute.firstChild, pageComponent });
}
return pageComponent ? null : parentRoute;
}
/**