Correct way to Build Array of Observables that http.get Data on Interval


#1

I’m trying to figure out the correct way to build an ion-list component (StocksList) that populates itself via a simple service (StocksListService) that instantiates new observable (Stock) class instances which get their data from a service (StockService) that calls an API repeatedly on an Observable.interval.

Ultimately I want to be able to create a stateful pipe to sort the Stocks[] (and I want to understand the actual correct way to do what I’m doing).

I don’t really know how to explain my intention concisely any better than that, so I’m hoping my code will help clarify the situation:

——

Home Page
^
Stocks List Component < Stocks List Service
^
Stock Component < Stock Service

——

Home Page

home.html

<ion-content>
  <ion-list
    stocksListCmp ></ion-list>
</ion-content>
home.ts

@Page({
  templateUrl: 'build/pages/home/home.html',
  directives: [StocksList],
  providers: [StocksListService]
})

export class HomePage {
  constructor(public nav: NavController) { }
}

Stocks List Component

stocks-list.tpl.html

<ion-card
  [stockCmp]="stock"
  *ngFor="#stock of stocks; #i = index"></ion-card>

stocks-list.component.ts

@Component({
  selector: '[stocksListCmp]',
  viewProviders: [StocksListService, StockService]
})

@View({
  templateUrl: 'build/components/stocks-list/stocks-list.tpl.html',
  directives: [Stock]
})

export class StocksList {
  stocks: Observable<IStock>[]; // not sure if my typing is correct here, or anywhere with advanced types really

  constructor(private stocksListService: StocksListService) { }

  ngOnInit() {
    this.stocks = this.stocksListService.stocks;
  }

}

stocks-list.service.ts

let tickers: string[] = ['AAPL', 'BTCUSD=X', '^DJI', '^GSPC', 'NFLX', 'TSLA'];

@Injectable()
export class StocksListService {

  constructor( @Inject(forwardRef(() => StockService)) private stockService: StockService) { }

  get stocks(): Observable<IStock>[] {
    return tickers
      .map((ticker) => new Stock(this.stockService.init(ticker))); // getting type error on this.stockService.init(ticker): Observable<Observable<IStock>> not assignable to StockService
  }

}

Stock Component

stock.tpl.html

<ion-card-content>
  <ion-row>
    <ion-col>{{ stock?.ticker }}</ion-col>
    <ion-col>{{ stock?.price }}</ion-col>
    <ion-col>{{ stock?.chg_percent}}</ion-col>
  </ion-row>
</ion-card-content>

stock.component.ts

@Component({
  selector: '[stockCmp]',
  inputs: ['stockCmp'],
  viewProviders: [StockService]
})

@View({
  templateUrl: 'build/components/stock/stock.tpl.html'
})

export class Stock {
  stockCmp: Stock; // not sure how to type here
  stock: IStock; // not sure how to type here

  constructor(private stockService: StockService) { }

  ngOnInit() {
    this.stockCmp['stockService']['init']
      .subscribe((data) => this.stock = data,
      null,
      () =>
        this.stockCmp['stockService']['update']
          .subscribe((service) => service
          .subscribe((data) => this.stock = data))
      );
  }

}

stock.service.ts

let source: Observable<number> = Observable.interval(60 * 1000).publish().refCount();

@Injectable()
export class StockService {

  constructor(private http: Http) { }

  public init(ticker: string): Observable<Observable<IStock>> {
    if (ticker) {
      let url = 'http://finance.yahoo.com/webservice/v1/symbols/' + ticker + '/quote?format=json&view=detail';
      return this.updateData(url);
    }
  }

  private loadData(url: string): Observable<IStock> {
    return this.http.get(url)
      .map((res) => this.mapData(res.json().list.resources[0].resource.fields));
  }

  private mapData(data: any): IStock {
    return {
      ticker: data.symbol,
      name: data.name,
      price: JSON.parse(data.price),
      chg_percent: JSON.parse(data.chg_percent) / 100,
      ts: data.ts
    };
  }

  public updateData(url: string): Observable<Observable<IStock>> {
    return { init: this.loadData(url), update: source.map(() => this.loadData(url)) }; // type error
  }

  private logError(error) {
    console.log(error);
  }

}

stock.d.ts

export interface IStock {
  ticker: string;
  name: string;
  price: number;
  chg_percent: number;
  ts: number;
}

So this actually works pretty well right now but I’m certain it’s not the correct way to do it. There has to be a better way to get the results from the loadData method before the first interval call.

I’d like to understand the correct way to do this in regards to building the ion-list, instantiating new class instances, and using reactive extensions and hopefully that will help me implement a stateful pipe to sort the stocks list.

Any advice is much appreciated! If I’ve left anything out or need to clarify anything, please let me know. Thanks!