2

Using Data/Action Streams via RxJS, I want to return/pass an error if a user inputs an invalid bay number. My code currently will return a bay object if it matches the number that the user inputs, BUT I can't figure out how to throw an error if the user inputs an invalid bay number that doesn't exist in my list of bays

  1. In order for me to share data amongst multiple components/pages, I've done most of my stuff in the BayService class:
  private baysUrl = 'api/bays';

  bays$ = this.http.get<Bay[]>(this.baysUrl)
    .pipe(
      tap(data => console.log('Bays: ', JSON.stringify(data))),
      catchError(this.handleError)
    );

  /*--------------------------------------------------------------*/
  // Grab A Single Bay
  private baySelectedSubject = new BehaviorSubject<number>(0);
  baySelectedAction$ = this.baySelectedSubject.asObservable();

  selectedBay$ = combineLatest([
    this.bays$,
    this.baySelectedAction$
  ])
    .pipe(
      map(([bays, selectedBayNumber]) =>
        bays.find(bay => bay.bayCode === selectedBayNumber)
      ),
    );

  selectedBayChanged(selectedBayNumber: number): void {
    this.baySelectedSubject.next(selectedBayNumber);
  }
  1. I created an Action stream by making a BehaviorSubject. Then I created a method to emit a value onto my Action stream. I then call this method in the bay.page.ts where I pass the input parameter and then emit it to my Data stream.
  2. I then combine my data stream and my action stream and then return a single bay object that matches the value from my action Stream.
  3. ISSUE: SO, I can already emit the value from a users input into my observable and return the bay object based on the number like so:
onSubmit() {
     this.bayDoesNotExistError = false;
      this.bayService.selectedBayChanged(this.bayForm.get('bayStart').value);
      this.navCtrl.navigateForward([`/results/`]);
      this.bayForm.reset();
    }

, but HOW do I handle an Invalid number? For example, How, when a user presses the submit button after typing in a number, check if it's invalid against my observable, and then return something back to my component to display on the UI? Currently, I client-side check what the user types and then display an error, But I need to actually check if the input value actually exists in my Observable<bay[]> objects and if it doesn't, return an error or something back to my bay.page.ts file to display in my HTML.

Sorry If I didn't explain this very well as I am trying to figure out how to word it.

Here's my BayService: BayService.ts

Here's my Bay.page.ts: Bay.page.ts

Donny groezinger
  • 127
  • 1
  • 3
  • 9
  • 1
    Hi Donny, Can you edit the post to include the code in the page? (instead of a picture) – bsheps Mar 01 '21 at 02:06
  • @bsheps Okay, I added my code! As you can see, it will handle a successful user input (1-3) and will display it because I only have 3 Bay Objects created and will navigate forward. But I want to check that the number is a valid bay number first, and if it isn't, somehow pass an error back to my bay.page.ts file and show it on my gui? Hopefully that helps – Donny groezinger Mar 01 '21 at 03:09

1 Answers1

0

I think the simplest solution would be to create a wrapper for your response.

export interface BaySelectionResponse {
 type: BayResponseType;
 message: string;
 bay: Bay;
}

export enum BayResponseType {
 ERR = "Bay Error",
 OK = "Success",
 NOT_IN_LIST = "BAY NOT FOUND IN LIST"
}

In this manner you can separate the responsibilities properly.

Your service will assign a status and message depending on the response, and your page can decide how to implement showing the result.

Using this approach you could even separate the result display into its own component with appropriate templates depending on whichever status your service returned.

Example

The Service

First, let's create a service which has the role of checking our remote for available data:

@Injectable({providedIn: 'root'})
export class ApiLookupService {
}

First we will need a method which will simply query our remote for all our possible values. I've mocked this out using the rxjs of function see here

  mockNumbersEndpoint() {
    return of([1, 3, 7, 9, 13])
  }

Secondly we will need a method, which will accept an input from our consumer (that is our routed component, which will utilize our service) which can check a response against a given input value.

  checkValidity(input: number): Observable<ValidityResponseModel> {
    // pipe the response from the api, and switch it into a ValidityResponseModel
    return this.mockNumbersEndpoint().pipe(
      switchMap(apiResponse => {
        const validity = {} as ValidityResponseModel;
        // check if the user's input is valid: Exists in response from server
        if (apiResponse.some(number => input == number)) {
          return of(this.createValidityResponse(
            `Your chosen value ${input} is available`,
            ValidityResponseTypes.SUCCESS,
            input
          ));
        }
        // if our code reaches this point, it means we did not find the users input
        return of(this.createValidityResponse(
          `Your chosen value ${input} is NOT available`,
          ValidityResponseTypes.INVALID,
          input
        ));
      }),
      // we have now left the level of the 'switch' and are back in the pipe.
      // We include catchError to handle if any error is thrown, such as network issues.
      catchError(error => {
        return of(this.createValidityResponse(
          "Something went wrong with your request",
          ValidityResponseTypes.ERR,
          input
        ));
      })
    )
  }

lastly, I added a utility function which restructures the response as a ValidityResponseType

  createValidityResponse(message: string, responseType: ValidityResponseTypes, response: number): ValidityResponseModel {
    return {
      message,
      responseType,
      response
    } as ValidityResponseModel
  }

Full Service Listing

@Injectable({providedIn: 'root'})
export class ApiLookupService {

  mockNumbersEndpoint() {
    return of([1, 3, 7, 9, 13])
  }

  checkValidity(input: number): Observable<ValidityResponseModel> {
    return this.mockNumbersEndpoint().pipe(
      switchMap(apiResponse => {
        const validity = {} as ValidityResponseModel;
        if (apiResponse.some(number => input == number)) {
          return of(this.createValidityResponse(
            `Your chosen value ${input} is available`,
            ValidityResponseTypes.SUCCESS,
            input
          ));

        }
        return of(this.createValidityResponse(
          `Your chosen value ${input} is NOT available`,
          ValidityResponseTypes.INVALID,
          input
        ));
      }),
      catchError(error => {
        return of(this.createValidityResponse(
          "Something went wrong with your request",
          ValidityResponseTypes.ERR,
          input
        ));
      })
    )
  }

  createValidityResponse(message: string, responseType: ValidityResponseTypes, response: number): ValidityResponseModel {
    return {
      message,
      responseType,
      response
    } as ValidityResponseModel
  }
  
}

The component which utilizes the service

Since, in the original question, you perform the check when the user hits enter, I've chosen to implement a form.

The component

@Component({
  templateUrl: './demo.component.html',
  styleUrls: ['./demo.component.scss']
})

export class DemoComponent implements OnInit {
  group: FormGroup;
  responseType = ValidityResponseTypes;
  validityResponse!: ValidityResponseModel;

  constructor(private fb: FormBuilder, private service: ApiLookupService) {
    this.group = fb.group({
      input: fb.control(0, [Validators.minLength(1)])
    })
  }

  ngOnInit(): void {
  }

  // ngSubmit.
  formSubmittal() {
    let v = this.group.get('input')?.value;
    if (this.group.dirty && this.group.valid) {
      this.group.reset();
      this.checkValidity(v);
    }
  }
  // pipe and take(1) to avoid having to manually unsubscribe.
  checkValidity(num: number) {
    this.service.checkValidity(num).pipe(take(1)).subscribe(responseFromApi => {
      // logic based on response here
      this.validityResponse = responseFromApi;
    })
  }
}

The Template

<h2>demonstration</h2>

<div class="method">
  <!-- ngSubmit will capture the Enter key event. -->
  <form [formGroup]="group" (ngSubmit)="formSubmittal()">
    <input type="number" formControlName="input">
  </form>
</div>

<div class="result" [ngClass]="{'success': validityResponse?.responseType == responseType.SUCCESS, 'invalid': validityResponse?.responseType == responseType.INVALID}">
  {{validityResponse?.message}}
</div>

Once we get a response from our service, we style according to the response type, and display the message prepared for us in the service.

Mikkel Christensen
  • 2,502
  • 1
  • 13
  • 22
  • I guess I'm confused on how I would implement that into my observable. That's the struggle that I'm having. I have a client-side input, in which I grab as an Action Stream (BehaviorSubject) in order to emit it to my Observable. It works perfectly if they enter a correct number. But if they enter an invalid number against my observable object, I need to return something back to say, the bay.page.ts file so that I can handle it and display on my GUI? I don't know how or I'm just getting confused on how to handle it. I added code to see and pictures – Donny groezinger Mar 01 '21 at 03:19
  • You need to subscribe to the `baySelectedSubject` inside of the component which is utilizing it. – Mikkel Christensen Mar 01 '21 at 08:58
  • Okay, but in my BayService class, do I need to have a: CatchError, if it can't find the specified bay based on the user input? Because I want the code to throw an error if it can't find that bay number and then handle that error in my bay.page.ts file where I can utilize it. – Donny groezinger Mar 01 '21 at 15:55
  • @Donnygroezinger I've updated my answer to reflect a full example – Mikkel Christensen Mar 02 '21 at 05:06