Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 75 additions & 28 deletions projects/components/view/src/view-data-source.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,75 @@
import { computed, signal, Signal } from '@angular/core';
import { computed, Resource, ResourceStatus, signal, Signal } from '@angular/core';
import { IZvException } from '@zvoove/components/core';
import { Observable, Subscription, first } from 'rxjs';
import { first, Observable, of, Subscription } from 'rxjs';

export interface IZvViewDataSource {
readonly contentVisible: Signal<boolean>;
readonly contentBlocked: Signal<boolean>;
readonly exception: Signal<IZvException | null>;
connect(): void;
disconnect(): void;
readonly error: Signal<unknown | null>;
readonly errorIcon: Signal<string | null>;
connect?(): void;
disconnect?(): void;
}

export interface ZvViewDataSourceOptions<TParams, TData> {
loadTrigger$: Observable<TParams>;
export interface ZvViewDataSourceOptions<TData, TParams = null> {
loadTrigger$?: Observable<TParams>;
loadFn: (params: TParams) => Observable<TData>;
keepLoadStreamOpen?: boolean;
}

export class ZvViewDataSource<TParams, TData> implements IZvViewDataSource {
private loading = signal<boolean>(false);
export class ZvViewDataSource<TData, TParams = null> implements IZvViewDataSource, Resource<TData> {
private blockView = signal<boolean>(false);

private connected = false;
private params: TParams | null = null;
private loadingSub = Subscription.EMPTY;
private connectSub = Subscription.EMPTY;
private loadtriggerSub = Subscription.EMPTY;

constructor(private options: ZvViewDataSourceOptions<TParams, TData>) {}
constructor(private options: ZvViewDataSourceOptions<TData, TParams>) {}

public result = signal<TData | null>(null);
public exception = signal<IZvException | null>(null);
public status = signal<ResourceStatus>(ResourceStatus.Idle);
public value = signal<TData>(null!);
public error = signal<unknown>(null);
public errorIcon = signal<string>('sentiment_very_dissatisfied');
public contentVisible = signal<boolean>(false);
public contentBlocked = computed(() => this.loading() || this.blockView());
public contentBlocked = computed(() => this.isLoading() || this.blockView());
public readonly isLoading = computed(() => this.status() === ResourceStatus.Loading || this.status() === ResourceStatus.Reloading);
public hasValue(): this is Resource<Exclude<TData, undefined>> {
return this.value() !== undefined;
}

/** @deprecated Use value() */
public result = computed(() => this.value());

/** @deprecated Use error() */
public exception = computed<IZvException | null>(() => (this.error() ? { errorObject: this.error(), icon: this.errorIcon() } : null));

/** @deprecated Use reload() */
public updateData() {
this.reload();
}

public connect() {
if (this.connected) {
throw new Error('ViewDataSource is already connected.');
}
this.connectSub = this.options.loadTrigger$.subscribe((params) => {
this.loadtriggerSub = (this.options.loadTrigger$ ?? of<TParams>(null!)).subscribe((params) => {
this.connected = true;
this.params = params;
this.loadData(params);
});
}

public updateData() {
public reload() {
if (!this.connected) {
throw new Error('ViewDataSource is not connected.');
}
this.loadData(this.params!);
return true;
}

public disconnect(): void {
this.connectSub.unsubscribe();
this.loadtriggerSub.unsubscribe();
this.loadingSub.unsubscribe();
}

Expand All @@ -61,29 +79,58 @@ export class ZvViewDataSource<TParams, TData> implements IZvViewDataSource {

private loadData(params: TParams) {
this.loadingSub.unsubscribe();
this.loading.set(true);
this.status.set(ResourceStatus.Loading);
this.contentVisible.set(true);
this.exception.set(null);
this.error.set(null);

let load$ = this.options.loadFn(params);
if (!this.options.keepLoadStreamOpen) {
load$ = load$.pipe(first());
}
this.loadingSub = load$.subscribe({
next: (result) => {
this.loading.set(false);
this.result.set(result);
this.status.set(ResourceStatus.Resolved);
this.value.set(result);
},
error: (err) => {
this.loading.set(false);
this.result.set(null);
this.status.set(ResourceStatus.Error);
this.value.set(undefined!);
this.contentVisible.set(false);
this.exception.set({
errorObject: err,
alignCenter: true,
icon: 'sentiment_very_dissatisfied',
});
this.error.set(err);
},
});
}
}

export class SignalZvViewDataSource<TData> implements IZvViewDataSource, Resource<TData> {
public readonly resource: Resource<TData>;
public readonly contentVisible = computed<boolean>(() => this.status() == ResourceStatus.Error);
public readonly contentBlocked = computed<boolean>(() => this.isLoading() || this.blockView());
public readonly errorIcon = signal<string>('sentiment_very_dissatisfied');

public readonly value: Signal<TData>;
public readonly status: Signal<ResourceStatus>;
public readonly error: Signal<unknown>;
public readonly isLoading: Signal<boolean>;
public hasValue(): this is Resource<Exclude<TData, undefined>> {
return this.resource.hasValue();
}

private blockView = signal<boolean>(false);

constructor(options: { resource: Resource<TData> }) {
this.resource = options.resource;
this.value = this.resource.value.bind(this.resource);
this.status = this.resource.status.bind(this.resource);
this.error = this.resource.error.bind(this.resource);
this.isLoading = this.resource.isLoading.bind(this.resource);
}

public reload() {
return this.resource.reload();
}

public setViewBlocked(value: boolean) {
this.blockView.set(value);
}
}
10 changes: 5 additions & 5 deletions projects/components/view/src/view.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
<ng-content></ng-content>
</zv-block-ui>
}
@if (dataSource.exception()) {
<mat-card class="zv-view__error-container" [class.zv-view__error-container--center]="dataSource.exception()?.alignCenter">
@if (dataSource.exception()?.icon) {
<mat-icon class="zv-view__error-icon">{{ dataSource.exception()?.icon }}</mat-icon>
@if (dataSource.error()) {
<mat-card class="zv-view__error-container">
@if (dataSource.errorIcon()) {
<mat-icon class="zv-view__error-icon">{{ dataSource.errorIcon() }}</mat-icon>
}
<span>{{ dataSource.exception()?.errorObject | zvErrorMessage }}</span>
<span>{{ dataSource.error() | zvErrorMessage }}</span>
</mat-card>
}
</div>
Expand Down
3 changes: 0 additions & 3 deletions projects/components/view/src/view.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@

mat-card.zv-view__error-container {
color: var(--zv-components-error);
}

mat-card.zv-view__error-container--center {
display: grid;
justify-items: center;
}
Expand Down
6 changes: 3 additions & 3 deletions projects/components/view/src/view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { IZvViewDataSource } from './view-data-source';
export class ZvView implements OnDestroy {
@Input({ required: true }) public set dataSource(value: IZvViewDataSource) {
if (this._dataSource) {
this._dataSource.disconnect();
this._dataSource.disconnect?.();
}

this._dataSource = value;
Expand All @@ -32,14 +32,14 @@ export class ZvView implements OnDestroy {

public ngOnDestroy() {
if (this._dataSource) {
this._dataSource.disconnect();
this._dataSource.disconnect?.();
}
}

private activateDataSource() {
if (!this._dataSource) {
return;
}
this._dataSource.connect();
this._dataSource.connect?.();
}
}
Loading