diff --git a/package.json b/package.json index 8c2546d26cc114a0336d4ac28d8ec2a7b7396c79..5ecc49077569747c214bf18d3e29aa01f07a5b98 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", "@ionic/pwa-elements": "~3.1.1", + "@rx-angular/state": "1.7.0", "@polkadot/api": "^9.2.4", "@polkadot/keyring": "^10.1.6", "@polkadot/networks": "^10.1.6", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 88f43782b5cd76e42013bd71c59a3a55b5685890..239a2167d97ec16fcc5f647a2d33bf90b6f61bc4 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -26,7 +26,12 @@ const routes: Routes = [ { path: 'settings', loadChildren: () => import('./settings/settings.module').then( m => m.SettingsPageModule) - } + }, + // DEV only + { + path: 'playground', + loadChildren: () => import('./playground/playground.module').then( m => m.PlaygroundModule) + }, ]; @NgModule({ diff --git a/src/app/playground/playground-routing.module.ts b/src/app/playground/playground-routing.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..66e5d776c2c22e0e8a5068bc698ea1894722b5ab --- /dev/null +++ b/src/app/playground/playground-routing.module.ts @@ -0,0 +1,18 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {HomePage} from "@app/home/home.page"; +import {PlaygroundPage} from "@app/playground/playground.page"; + +const routes: Routes = [ + { + path: '', + pathMatch: 'full', + component: PlaygroundPage + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class PlaygroundPageRoutingModule {} diff --git a/src/app/playground/playground.module.ts b/src/app/playground/playground.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..99ffa8078d170aa20fda940343c7082d675014f5 --- /dev/null +++ b/src/app/playground/playground.module.ts @@ -0,0 +1,19 @@ +import {NgModule} from "@angular/core"; +import {AppSharedModule} from "@app/shared/shared.module"; +import {TranslateModule} from "@ngx-translate/core"; +import {AppAuthModule} from "@app/auth/auth.module"; +import {AppRegisterModule} from "@app/register/register.module"; +import {PlaygroundPage} from "@app/playground/playground.page"; +import {PlaygroundPageRoutingModule} from "@app/playground/playground-routing.module"; + +@NgModule({ + imports: [ + AppSharedModule, + TranslateModule.forChild(), + PlaygroundPageRoutingModule, + AppAuthModule, + AppRegisterModule + ], + declarations: [PlaygroundPage] +}) +export class PlaygroundModule {} diff --git a/src/app/playground/playground.page.html b/src/app/playground/playground.page.html new file mode 100644 index 0000000000000000000000000000000000000000..d41d3da40e0bacaf3075ca7bfe9a36053c4d31cd --- /dev/null +++ b/src/app/playground/playground.page.html @@ -0,0 +1,8 @@ + +<ion-content> + + <h1>Playground</h1> + + <div>{{ state$ | async | json }}</div> + +</ion-content> diff --git a/src/app/playground/playground.page.ts b/src/app/playground/playground.page.ts new file mode 100644 index 0000000000000000000000000000000000000000..f0edd3e8a93234fae142705c4480b29e52e6c1c6 --- /dev/null +++ b/src/app/playground/playground.page.ts @@ -0,0 +1,35 @@ +import {Component, Injector} from "@angular/core"; +import {RxBasePage} from "@app/playground/rx-base.page"; +import {RxState} from "@rx-angular/state"; +import {interval, map, mapTo} from "rxjs"; + + +export declare interface PlaygroundState { + bar: number; + foo: string; +} + +@Component({ + selector: 'app-playground', + templateUrl: './playground.page.html', + //styleUrls: ['./playground.page.scss'] + providers: [RxState] +}) +export class PlaygroundPage extends RxBasePage<PlaygroundState> { + readonly state$ = this.state.select(); + + constructor(injector: Injector, + private state: RxState<PlaygroundState>) { + super(injector); + state.set({bar: 0, foo: 'foo'}) + const sliceToAdd$ = interval(250) + .pipe(map((i) => { + return { bar: 5 * i, foo: 'foo'}; + })); + state.connect(sliceToAdd$); + } + + protected ngOnLoad(): Promise<PlaygroundState> { + return Promise.resolve(undefined); + } +} diff --git a/src/app/playground/rx-base.page.ts b/src/app/playground/rx-base.page.ts new file mode 100644 index 0000000000000000000000000000000000000000..0f980796cc6c40bd657621e7ccf19db6cec5695c --- /dev/null +++ b/src/app/playground/rx-base.page.ts @@ -0,0 +1,170 @@ +import {ChangeDetectorRef, Directive, inject, Injector, OnDestroy, OnInit} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {SettingsService} from "@app/settings/settings.service"; +import {changeCaseToUnderscore, isNotNilOrBlank} from "@app/shared/functions"; +import {environment} from "@environments/environment"; +import {waitIdle} from "@app/shared/forms"; +import {WaitForOptions} from "@app/shared/observables"; +import {ToastController, ToastOptions} from "@ionic/angular"; +import {TranslateService} from "@ngx-translate/core"; +import {Subscription} from "rxjs"; + +export interface BasePageOptions { + name: string; + loadDueTime: number; +} + +@Directive() +export abstract class RxBasePage< + S, + O extends BasePageOptions = BasePageOptions + > + implements OnInit, OnDestroy { + + private _cd: ChangeDetectorRef; + private _subscription: Subscription; + + protected translate: TranslateService; + protected settings: SettingsService; + protected readonly activatedRoute: ActivatedRoute; + protected toastController: ToastController; + protected readonly _debug = !environment.production; + protected readonly _logPrefix: string; + protected readonly _options: O; + protected _presentingElement: Element = null; + + mobile: boolean = null; + error: string = null; + loading = true; + data: S = null; + + get loaded(): boolean { + return !this.loading; + } + + protected constructor( + injector: Injector, + options?: Partial<O> + ) { + this._cd = injector.get(ChangeDetectorRef); + this.settings = injector.get(SettingsService); + this.translate = injector.get(TranslateService); + this.activatedRoute = injector.get(ActivatedRoute); + this.toastController = injector.get(ToastController); + this.mobile = this.settings.mobile; + this._options = <O>{ + name: options?.name || changeCaseToUnderscore(this.constructor.name).replace(/_/g, '-'), + loadDueTime: 0, + ...options + }; + this._logPrefix = `[${this._options.name}] `; + } + + ngOnInit() { + + // Get modal presenting element (iOS only) + this._presentingElement = this.mobile ? document.querySelector('.ion-page') : null; + + // Load data + setTimeout(() => this.load(), this._options.loadDueTime || 0); + } + + ngOnDestroy() { + console.debug(`${this._logPrefix}Destroy`); + this._subscription?.unsubscribe(); + } + + protected async load() { + this.resetError(); + this.markAsLoading(); + + try { + + this.data = await this.ngOnLoad(); + + this.markForCheck(); + this.markAsLoaded(); + } + catch (err) { + this.setError(err); + this.markAsLoaded(); + } + } + + protected abstract ngOnLoad(): Promise<S>; + + protected setError(err, opts = {emitEvent: true}) { + let message = err?.message || err || 'ERROR.UNKNOWN_ERROR'; + if (!message) { + console.error(err); + message = 'ERROR.UNKNOWN_ERROR'; + } + this.error = message; + if (opts.emitEvent !== false) this.markForCheck(); + } + + protected resetError(opts ={emitEvent: true}) { + if (this.error) { + this.error = null; + if (opts.emitEvent !== false) this.markForCheck(); + } + } + + protected markAsLoading(opts = {emitEvent: true}) { + if (!this.loading) { + this.loading = true; + if (opts.emitEvent !== false) this.markForCheck(); + } + } + + protected markAsLoaded(opts = {emitEvent: true}) { + if (this.loading) { + this.loading = false; + if (opts.emitEvent !== false) this.markForCheck(); + } + } + + protected async waitIdle(opts?: WaitForOptions) { + return waitIdle(this, opts); + } + + protected markForCheck() { + this._cd?.markForCheck(); + } + + protected debug(msg, ...params: any[]) { + if (!this._debug) return; + if (params) console.debug(this._logPrefix + msg, params); + else console.debug(this._logPrefix + msg) + } + + protected info(msg, ...params: any[]) { + if (params) console.info(this._logPrefix + msg, params); + else console.info(this._logPrefix + msg) + } + + protected log(msg, ...params: any[]) { + if (!this._debug) return; + if (params) console.log(this._logPrefix + msg, params); + else console.log(this._logPrefix + msg) + } + + protected async showToast(opts: ToastOptions & {messageParams?: any[] }) { + const message = isNotNilOrBlank(opts?.message) ? this.translate.instant(opts.message as string, opts.messageParams) : undefined; + const toast = await this.toastController.create({ + duration: 2000, + ...opts, + message + }); + return toast.present(); + } + + protected registerSubscription(sub: Subscription) { + if (!this._subscription) this._subscription = new Subscription(); + this._subscription.add(sub); + } + + protected unregisterSubscription(sub: Subscription) { + this._subscription?.remove(sub); + } +}