Execute code after executing everything for loop

Viewed 935

Please let me know if I have to provide any more code or explanations.

I am trying to execute validate, if validation not successful, confirm(Yes/No), and then save method for all selected rows in a for loop.

Requirement: I have show a success/failure message after looping through all selected rows. That means:

If 5 rows are selected (2 are valid and 3 are not valid) - got from myService.validate

It will ask if you still want to continue for 3 not valid records - myConfirmService.confirm

It should save those 2 valid records

If user selects 'Yes' in confirm window (lets says for 2 records), then it should save these 2 as well.

At the end - it will display success/failure message - alertService.success

What I want:

To fulfil what I described in the requirement

and The success/failure message should display only once after all record saved

component.ts:

save() {
    this.selectRowData.forEach(async s => {
        this.myService.validate(s.id).subscribe(data => {
            console.log(data);
            if(data == false) {
                this.myConfirmService
                    .confirm('Please confirm..', 'Selected Part do not match expected part data. \n Do you still want to continue? ', 'Yes', 'No')
                    .then((confirmed) => {
                        if(confirmed) {
                            console.log("User selected: Yes");
                            this.myService.savePart();
                        } else {
                            console.log("User selected: No");
                        }
                    })
                    .catch(() => {
                        console.log("Error...");
                    });
            } else {
                console.log("Selected Rule matches expected rules for: " + s.designChangeClass);
                this.saveModelPart(s);
            }
        }, (error: any) => {
            alert("Error Linking Lot");
        });
    });
    console.log('1st After for loop');
    this.alertService.success('Part saved successfully.')
}

service.ts

validate(id: string): Observable < boolean > {
    return this.httpClient.get < boolean > ('/RestWeb/validate?id=' + id);
}
      
validate1(id: string) {
    return this.httpClient.get < boolean > ('/RestWeb/validate?id=' + id).toPromise();
}
  
savePart(item: Part): Observable<any> {
    return this.httpClient.post('/RestService/savePart', item);
}
4 Answers

Here you go.

//Change method to async
async save() {
  //Create custom index variable, since for of loop doesn't have one
  let ind = 0;
  //Use for of instead of forEach
  for (const eachRow of this.selectRowData) {
    //await until API validated
    const isValid = await this.myService.validate(eachRow.id).toPromise();
    if (!isValid) {
      //await again if it is not valid and until user confirmed
      const isConfirmed = await this.myConfirmService.confirm('Please confirm..', 'Selected Part do not match expected part data. \n Do you still want to continue? ', 'Yes', 'No').toPromise();
      if (isConfirmed) {
        //await again until record saved as user selected yes in confirm
        console.log('User selected: Yes');
        await this.myService.savePart().toPromise();
      } else {
        console.log('User selected: No');
      }
    } else {
      console.log(
        'Selected Rule matches expected rules for: ' +
          eachRow.designChangeClass
      );
      this.saveModelPart(eachRow);
    }
    //Finally display message after loop is finished
    if (ind === this.selectRowData.length - 1) {
      this.alertService.success('Part saved successfully.');
    }
    ind++;
  }
}

Although this solution works but is not a good practice to do it in this way,
Instead, modify API to accept multiple ids for validation, and display a table of records (success and failures) along with buttons to confirm for continuation.

You could use fork join in this case, so it makes all calls and get the response on a arrays of booleans

save() {
  let obsArray = this.selectRowData.map(s=>this.myService.validate(s.id));
  forkJoin(obsArray).subscribe(
    (arrayBoolean)=>{
       // do your logic here
    }
  )
 
}

See https://www.learnrxjs.io/learn-rxjs/operators/combination/forkjoin for more info

//EDIT: misspelled

For what i understood your trying to save only valid data with an extra of user confirmation. One Part is a little confusing because you are trying to confirm valid data that does not match and expected rule but apart from that, i would do something like this.

Component method:

  rowValidation() {
    this.selectRowData.forEach( rowElement => {
      this.myService.validate(rowElement.id).subscribe( data => {
        //call comfirm service method.
      })
    })
  }

Confirm Service method:

  confirm() {
    //ask for confirmation code
    //if(yes) call myservice.save
    //else log "no"
  }

Alright friend... your logic is actually a bit complicated, so it will be much easier to follow if we break it up into smaller parts.

If I understand your requirements correctly, you need to:

  1. Validate each row
  2. If any fail validation, ask user if they want to save anyway
  3. Save data (valid rows, and possibly invalid rows)

Starting with the end in mind, we could do something like this:

  save() {
    of(this.selectRowData).pipe(
      switchMap(this.validateRows),
      switchMap(rows => this.promptUserIfSomeInvalid(rows).pipe(
        switchMap(saveInvalidRows => {
          const rowsToSave = saveInvalidRows ? rows : rows.filter(r => r.isValid);
          
          return forkJoin(rowsToSave.map(this.myService.savePart));
        })
      ))
    ).subscribe();
  }

Here we start the observable chain with of.

You may have noticed all the switchMaps!!! If you aren't familiar, switchMap is a nifty operator that will subscribe to an observable for you, and emit its emissions. It also takes care of unsubscribing from this "inner observable" automatically.

What you need to know about switchMap:

Inside switchMap, you pass a function that returns an observable.

The first switchMap receives the row data and subscribes to an observable that calls your validation observable. this.validateRows returns an observable that emits the array of rows, each with an additional boolean property indicating if the validation for that row passed.

switchMap #2 subscribes to an observable that emits a boolean indicating whether or not invalid rows should be saved.

switchMap #3 subscribes to an observable that makes all your api calls to save the appropriate rows.

forkJoin creates an observable that emits when its array of source observables all complete. So, we are simply mapping the rows, to observables (this.myService.savePart()).

Here are the implementations of the methods used above:

  private validateRows(rows: Row[]) {
    return forkJoin(rows.map(row => this.myService.validate(row.id))).pipe(
      map(this.appendIsValidFieldToRows(rows)),
    );
  }

  private promptUserIfSomeInvalid(rows: ValidatedRow[]) {
    const invalidCount = rows.filter(r => !r.isValid).length;
    const message = `Would you like to save ${invalidCount} rows?`;

    return rows.some(r => !r.isValid)
      ? from(this.myConfirmService.confirm(message))
      : of(true);
  }

  private appendIsValidFieldToRows(rows: Row[]) {
    return (isValidArray: boolean[]) => rows.map((r, i) => ({...r, isValid: isValidArray[i]})) as ValidatedRow[];
  }

Here is a Non-Functional StackBlitz that I used as a scratch pad to avoid typing mistakes. It may be useful for seeing the typings or even throw some fake data into for testing.

I'm not 100% sure I understood your exact intention, but I hope this gives you a push in the right direction.

The main takeaways are:

  • Break code into smaller functions
  • Utilize switchMap to avoid nested subscriptions.
  • Instead of using forEach loops, use Array.prototype.map to transform the array of rows into an array of Observables, that you can then pass to forkJoin

Note: I left out your error catching because this answer was getting pretty long and the question didn't really focus on that aspect. However, with the logic broken down into separate functions, it makes it easy to add in a catchError in the necessary places.

Related