3
\$\begingroup\$

Here is a function that triggers after selecting from a dropdown to choose the floor. As you can see, there are 3 levels of API calls (regular HTTP GET requests with JSON responses) that chain together. The first 2 are regular requests and the 3rd one polls. How can I write this in a better way that utilizes RXJS and observable functions?

onChange(floor)
 {
    this.selectedFloor = floor;
    this.floorService.getRoomsInFloor(floor).subscribe((rooms:IRoom[])=>{
        rooms.forEach((room:IRoom)=>{
            this.floorService.getDeviceInRoom(room.roomId).subscribe((device:IDevice)=>{    
                this.floorService.getLatestStatusOfDeviceInRoom(device.deviceId).subscribe((status:IStatus)=>{
                    if(status)
                    {
                    status.occupied == "true"? this.rooms[room.roomId][4] = "red" : this.rooms[room.roomId][4] = "green";

                    }

                    this.renderRooms();
                });
            });
        });
    });
 }

getRoomsInFloor(floorid)
{

    return this.http.get(`example.com/floor/${floorid}/rooms`).map(res => res.json()).catch(this._serverError)
}

getDeviceInRoom(roomid)
{
    return this.http.get(`example.com/devices/${roomid}`).map(res => res.json()).catch(this._serverError)

}

getLatestStatusOfDeviceInRoom(deviceid)
{
    return Observable.timer(0,2000).switchMap(()=> this.http.get(`example.com/roomPresences/${deviceid}/latest`).map(res => res.json()).catch(this._serverError));
}
\$\endgroup\$
2
\$\begingroup\$

First of all, you are using some kind of C# code style in your typescript. I suggest the ng lint command-line to conform with typescript/angular code style. Moreover prefixing interfaces with I is not a typescript convention (https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines).

I think the difficult part of your code with rxjs is how to know which room we have to update when we receive a status.

I find a way to keep track of the room without changing the api of the floorService via zipping the observables returned by the floorService with an observable containing the room.

Here is a proposal with rxjs, I tried to fill the blanks in your posted code to make it run so focus on the zipWithRoom function and the onChange method:

the component with the dropdown:

import { Component } from '@angular/core';
import { FloorService } from './floor.service';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/zip';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/filter';


export interface IRoom {
  roomId: number;
}

export interface IDevice {
  deviceId: number;
}

export interface IStatus {
  occupied: string;
}

function zipWithRoom<T>(observable: Observable<T>, theRoom: IRoom) {
  return Observable.zip(observable, Observable.of(theRoom), (result: T, room: IRoom) => ({result, room}));
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  selectedFloor;
  rooms: { [roomId: number]: Array<String> };

  constructor(private floorService: FloorService) {
  }

  onChange(floor: any) {
    this.selectedFloor = floor;

    this.floorService.getRoomsInFloor(floor)
    // we create a sequence from the room array
      .mergeMap((rooms: Array<IRoom>): Observable<IRoom> => Observable.from(rooms))
      .mergeMap((room: IRoom) =>
        zipWithRoom<IDevice>(this.floorService.getDeviceInRoom(room.roomId), room))
      .mergeMap(({result, room}) =>
        zipWithRoom<IStatus>(this.floorService.getLatestStatusOfDeviceInRoom(result.deviceId), room))
      .filter(({result, room}) => !!result)
      .subscribe(({result, room}) => {
        // little change to the following line to make it more readable
        this.rooms[room.roomId][4] = result.occupied === 'true' ? 'red' : 'green';
        this.renderRooms();
      });
  }

  private renderRooms() {
  }
}

spec file:

import { TestBed, async } from '@angular/core/testing';

import { AppComponent } from './app.component';
import { FloorService } from './floor.service';
import { Observable } from 'rxjs/Observable';

describe('AppComponent', () => {

  const mockedFloorService = jasmine.createSpyObj(['getRoomsInFloor', 'getDeviceInRoom', 'getLatestStatusOfDeviceInRoom']);

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
      providers: [
        {provide: FloorService, useValue: mockedFloorService}
      ]
    }).compileComponents();
  }));

  it('should update the status of the rooms', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;

    app.rooms = {
      1: [],
      2: []
    };

    mockedFloorService.getRoomsInFloor.and.returnValue(Observable.of([{roomId: 1}, {roomId: 2}]));
    mockedFloorService.getDeviceInRoom.and.returnValues(Observable.of({deviceId: 11}), Observable.of({deviceId: 12}));
    mockedFloorService.getLatestStatusOfDeviceInRoom.and.returnValues(
      Observable.of({occupied: 'true'}),
      Observable.of({occupied: 'false'})
    );

    app.onChange(2);
    expect(mockedFloorService.getRoomsInFloor).toHaveBeenCalledWith(2);
    expect(app.rooms[1][4]).toBe('red');
    expect(app.rooms[2][4]).toBe('green');
  }));
});
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.