angular - Använder Observables, visa laddningsindikator efter en fördröjning men avbryt om laddningen är klar i tid?


I min kund-detaljkomponent har jag följande kod som uppnår det jag är ute efter, men inte på det reaktiva/observerbara sätt som jag tror kan vara möjligt.

Istället för att omsluta this.isLoading=true; i ett if-uttryck, finns det ett sätt att göra detta med tekniker för reaktiv programmering? Kanske genom att avbryta/ignorera det fördröjda observabla om kunden hämtas först? Eller, går jag tillväga på fel sätt?

export class CustomerDetailComponent implements OnInit {

  customer: Customer;
  errorMessage: string;
  isLoading: boolean;

  constructor(
    private customerService: CustomerService,
    private route: ActivatedRoute,
    private router: Router,
    private location: Location
  ) { }

  ngOnInit() { 
    let idParam = this.route.params
      .distinctUntilChanged(params => params['id']);

    idParam.subscribe(params => 
    {
      this.errorMessage = ''; 
    });

    idParam.delay(300).subscribe(params => 
    {
      if (!(this.customer && this.customer.id == params['id']))
        this.isLoading = true;
    });

    idParam.switchMap((params: Params) => this.customerService.getCustomer(params['id']))
      .subscribe(customer => 
      { 
        this.customer = customer; 
        this.isLoading = false;
      },
      error => this.errorMessage = error);
  }
}

Svar #1

Du kan skriva något i stil med detta:

function getCustomer(id) {
    return Observable.of({'name': 'John', id}).delay(500);
}

Observable.of({'id': 42})
    .distinctUntilChanged(params => params['id'])
    .do(() => {
        // this.errorMessage = '';
    })
    .switchMap((params) => {
        return Observable.combineLatest(
            Observable.of(true).delay(300).startWith(null), // delay Observable
            getCustomer(params['id']).startWith(null), // customer Observable
            function(delay, customer) { // selector function
                if (customer) {
                    return customer;
                }

                if (delay && !customer) {
                    console.log('this.isLoading = true;');
                }
                return null;
            })
            .filter(customer => customer)
            .distinctUntilChanged(customer => customer['id']);
    })
    .subscribe(
        customer => {
            console.log('this.isLoading = false;');
            console.log(customer);
            // this.customer = customer;
        },
        error => {
            // this.errorMessage = error;
        }
    );

Se live-demo: https://jsbin.com/nebutup/5/edit?js,console

Den inre combineLatest() tar emot två Observables:

  1. Förseningen på 300 ms
  2. Kunden från en fjärrtjänst (i denna demo simulerad)

Sedan finns det också en projektionsfunktion som används för att välja vad vi vill sprida vidare. Båda Observables använder .startWith(null) för att säkerställa att de har minst ett objekt som skickas ut så att combineLatest() triggas av en förändring i någon av dem. Då kan vi enkelt veta om det första Observable som skickade ut var fördröjningen eller kunden.

Sedan finns det också filter() för att ta bort alla null-värden och distinctUntilChanged() för att se till att vi inte skickar samma kund två gånger (detta hanterar fallet där kunden blir klar först).

När vi sedan kör denna demo och fördröjningen utlöses först är utdata följande:

this.isLoading = true;
this.isLoading = false;
{ name: 'John', id: 42 }

Detta betyder att vi först visar laddningen och sedan döljer den.

Sedan när vi ändrar getCustomer() för att slutföra först:

function getCustomer(id) {
    return Observable.of({'name': 'John', id}).delay(100);
}

vi får följande:

this.isLoading = false;
{ name: 'John', id: 42 }

Detta betyder att vi aldrig visar någon inladdning.

Svar #2

Här är ett rxjs 6 pipat tillvägagångssätt med en återanvändbar operator:

export function delayIndicator<T>(delay: number, start: () => void, complete: () => void): OperatorFunction<T, T> {
  const loadingShown$ = timer(delay).pipe(
    tap(() => start()),
    mapTo(true),
    startWith(false)
  );

  return (input$) =>
    combineLatest([input$, loadingShown$]).pipe(
      take(1),
      map(([input, delayShown]) => {
        if (delayShown) {
          complete();
        }

        return input;
      })
    );
}

myObservable$.pipe(delayIndicator(300, () => this.loading = true, () => this.loading = false));

Relaterade artiklar