I'm building an Angular Material mat-tree
with dynamic data (based on this example).
When the user expend a node, a progress-bar is shown while the list of child nodes is retrieved from the server.
The base code looks like this:
toggleNode(node: DynamicFlatNode, expand: boolean) {
if (expand) {
// Shows progress bar
node.isLoading = true;
this
.database
.getChildren(node.id)
.subscribe(
values => {
// Create and add the descendant nodes
...
},
error => { console.error(error); },
() => {
// Hides progress bar
node.isLoading = false;
});
} else {
// Remove descendant nodes
...
}
}
The issue is that if the data retrieving is too quick, the progress bar just blink.
I would like to add a delay so the progress bar is shown at least, let say, 250ms.
I want the time of visibility to be min(250ms, data retrieving time)
not 250 ms data retrieving time
.
My current solution is to use a dummy promise, that starts before data retrieving and is awaited in the complete
callback:
toggleNode(node: DynamicFlatNode, expand: boolean) {
if (expand) {
// Shows progress bar
node.isLoading = true;
const delay = new Promise(resolve => setTimeout(resolve, 250));
this
.database
.getChildren(node.id)
.subscribe(
values => {
// Create and add the descendant nodes
...
},
error => { console.error(error); },
async () => {
await delay;
// Hides progress bar
node.isLoading = false;
});
} else {
// Remove descendant nodes
...
}
}
Is there a more concise way of doing this?
CodePudding user response:
You could leverage the RxJS timer
with forkJoin
to introduce an artificial delay. I'd also suggest using the finalize
operator to set the isLoading
flag. If not you'd have to set it in both error
and complete
callbacks of the subscription.
Important: forkJoin
will only emit when all it's source observables complete. So you need to make sure your observable this.database.getChildren(node.id)
completes. If it doesn't you could force it to complete after the first emission using take(1)
, assuming you wouldn't need the emission stream.
Try the following
import { forkJoin, timer } from 'rxjs';
import { finalize, map, take } from 'rxjs/operators';
const DELAY = 250;
toggleNode(node: DynamicFlatNode, expand: boolean) {
if (expand) {
node.isLoading = true; // <-- show progress bar
forkJoin({
timer: timer(DELAY),
children: this.database.getChildren(node.id),
}).pipe(
map(({ children }) => children), // <-- ignore the `timer` emission
finalize(() => (node.isLoading = false)) // <-- hide progress bar
).subscribe({
next: (values: any) => {
// create and add the descendant nodes
},
error: (error: any) => {
// handle error
},
});
} else {
// remove descendant nodes
}
}
Working example: Stackblitz. Play with the DELAY
variable to observe the induced delay.
CodePudding user response:
You can grab idea from here
import { of, map, Observable, combineLatest, delay, timer } from 'rxjs';
import { mapTo, startWith, tap } from 'rxjs/operators';
const GET_DATA_DELAY = 100;
const PROGRESS_DELAY = 250;
const getData = (): Observable<any> => {
return of('data').pipe(delay(GET_DATA_DELAY));
};
const log = {
next: (value: any) => console.log(`Date:${ (new Date()).toISOString()} : `, value),
error: (err: any) => console.error(err),
};
const delay$ = timer(PROGRESS_DELAY).pipe(mapTo('timer'),tap(log));
const data$ = getData().pipe(tap(log));
const isLoading$ = combineLatest([delay$, data$]).pipe(
mapTo(false),
startWith(true),
map(value => `IsLoading: ${value}`),
);
isLoading$.subscribe(log);
// Open the console in the bottom right to see results.