Search code examples
angularreactive-programmingionic2angular-providers

Angular 2 Component not updating using *ngFor with Reactive Provider


I am trying to create an Angular 2 Provider that does all the CRUD operations while using a reactive approach. However, my Ionic App adds to my list when I call loadAll() the first time, but anytime I call this.children.next(children) after that, it does not update the list.

Also, I call this.childrenService.createChild(child), no child is updated as well.

My environment:

Running on Windows 10, and running ionic --version gives me 2.0.0-beta.35

Part of my package.json:

...
"@angular/common": "2.0.0-rc.4",
"@angular/compiler": "2.0.0-rc.4",
"@angular/core": "2.0.0-rc.4",
"@angular/forms": "0.2.0",
"@angular/http": "2.0.0-rc.4",
"@angular/platform-browser": "2.0.0-rc.4",
"@angular/platform-browser-dynamic": "2.0.0-rc.4",
"ionic-angular": "2.0.0-beta.11",
"ionic-native": "1.3.10",
"ionicons": "3.0.0"
...

Provider Class:

import {Injectable, EventEmitter} from '@angular/core';
import { Http } from '@angular/http';
import {Child} from "../../models/child.model";
import {Constants} from "../../models/Constants";
import 'rxjs/Rx';
import {Observable, Subject, ReplaySubject} from "rxjs";
import {IChildOperation} from "../../models/IChildOperation";

@Injectable()
export class ChildrenService {

  public children: Subject<Child[]> = new Subject<Child[]>();
  private updates: Subject<IChildOperation> = new Subject<IChildOperation>();
  private createStream: Subject<Child> = new Subject<Child>();

  constructor(private http: Http) {
    this.updates
    .scan((children : Child[], operation: IChildOperation) => {
        return operation(children);
      }, [])
    .subscribe(this.children);

    this.createStream
    .map((newChild: Child) => {
      //"Do what is in this returned method for each child given to this create object (via this.createStream.next(newChild))"
      return (curChildren: Child[]) => {
        console.log("Creating a new child via stream.");
        return curChildren.concat(newChild);
      }
    })
    .subscribe(this.updates);

    this.loadAll();
  }

  createChild(newChild: Child) {
    console.log("Creating child...");
    this.http.post(`${Constants.CHILDREN_API}`, newChild)
    .map(res=>res.json())
    .subscribe((newChild: Child) => {
      console.log("Child Created!");
      console.log(newChild);
      this.createStream.next(newChild);
    });
  }

  loadAll() {
    this.http.get(`${Constants.CHILDREN_API}`)
    .map(res => res.json())
    .subscribe(
      (children: Child[]) => { // on success
        console.log(children);
        this.children.next(children);
      },
      (err: any) => { // on error
        console.log(err);
      },
      () => { // on completion
      }
    );
  }
}

Home Component

import {Component, OnInit, ChangeDetectionStrategy} from '@angular/core';
import {NavController, ModalController} from 'ionic-angular';
import {AddChildPage} from "../add-child/add-child";
import {ChildrenService} from "../../providers/children/ChildrenService";
import {Child} from "../../models/child.model";
import {AsyncPipe} from "@angular/common";

@Component({
  templateUrl: 'build/pages/home/home.html',
  providers: [ ChildrenService ],
  pipes: [AsyncPipe],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomePage implements OnInit {
  constructor(
    private nav: NavController,
    private childrenService: ChildrenService,
    private modalCtrl: ModalController) {}
  }

  goToAddChild() {
    this.nav.push(AddChildPage);
  }
}

Home template

...
  <ion-list>
    <ion-item *ngFor="let child of childrenService.children | async">
      {{ child.name }}
    </ion-item>
  </ion-list>

  <button secondary fab fab-fixed fab-bottom fab-right margin
          (click)="goToAddChild()"><ion-icon name="person-add"></ion-icon></button>
...

AddChild Component

import {Component, Output} from '@angular/core';
import {NavController, NavParams, ViewController} from 'ionic-angular';
import {ChildrenService} from "../../providers/children/ChildrenService";
import {Child} from "../../models/child.model";

@Component({
  templateUrl: 'build/pages/add-child/add-child.html',
  providers: [ ChildrenService ]
})
export class AddChildPage {
  newChild: Child;

  constructor(private nav: NavController, private childrenService: ChildrenService, public viewCtrl: ViewController) {
    this.newChild = new Child({ name: "" });
  }

  addChild() {
    this.childrenService.createChild(this.newChild);
    this.dismiss();
  }

  dismiss() {
    this.viewCtrl.dismiss();
  }
}

UPDATE: I tested my Reactive Provider with only Angular 2 (without Ionic 2) and If I change these two things in my provider:

export class ChildrenService {

  //Changed child from Subject to ReplaySubject
  public children: ReplaySubject<Child[]> = new ReplaySubject<Child[]>();
  ...


  createChild(newChild: Child) {
    console.log("Creating child...");
    this.http.post(`${Constants.CHILDREN_API}`, newChild)
    .map(res=>res.json())
    .subscribe((newChild: Child) => {
      console.log("Child Created!");
      console.log(newChild);
      this.createStream.next(newChild);
      this.loadAll(); //This is the NEW line in createChild
    });
  }

When I changed children to a ReplaySubject instead of a Subject, as well as called this.loadAll() in the function that is called for a new child, the list of children update in Angular, but does not update in my Ionic app.

I did notice that the curChildren (found in the function in createStream) is always empty. I assumed that the curChildren would be the ones currently displayed.

It prints out all the expected logs saying its creating the child, it does the post request right, after I get the request back, it says that its creating the child via the stream but nothing updates.

I think this could be Ionic not updating via the stream properly or I am not using rxjs properly. What could it be?

Thanks


Solution

  • Three main changes were needed to fix this:

    • Remove AddChildComponent - Add the child directly in HomePage
    • Change the children property in ChildrenService to an Observable
      instead of a Subject/ReplaySubject
    • Instead of: this.updates.scan(...).subscribe(this.children), change it to this.children = this.updates.scan(...)

    ChildrenService.ts

    ...
    export class ChildrenService {
    
      public children: Observable<Child[]> = new Observable<Child[]>();
      ...
    
      constructor(private http: Http) {
        //AsyncPipe will take care of subscribing, so just set it to this.updates
        this.children = this.updates
        .scan((children : Child[], operation: IChildOperation) => {
            return operation(children);
          }, []);
    
        ...
      }
    }
    

    Take away: AsyncPipe works great for this problem. However, I'm still not sure why I had to remove the AddChild component, since all the creation logic is in the ChildrenService.