Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular2 how to update an item inside an observable collection

Update, in short:

I am looking for the equivalent of doing something like this but for an Observable rather than a regular array:

var i = this.customers.findIndex(customer => customer._id === id);
~i && this.customers[i] = newObject.

I have 2 components on the screen. A list component on the left and a display component on the right (imagine it to be a PDF that is just rendering the latest 'version' of data)

When you click an item in the list, it displays the data for that selected item in the component on the right.

The list is an observable array:

items$: Observable<Proposal[]>;

Each item in the list has a child component. It is possible to click an icon on a single item, which changes the data of that child. The child has an event emitter to tell the parent the data has changed:

@Output() proposalDataChanged: EventEmitter<string> = new EventEmitter();

The parent binds to it:

 <fb-proposal-list-item
  [proposal]="proposal" (proposalDataChanged)="handleDataChanged(p)">
 </fb-proposal-list-item>

The problem I have is that in the handleDataChanged method I want to search the Observable for the item that has changed and replace it with the new payload returned from the emitter. I do not want to call the server to refresh the entire list.

I need to do this so that the component on the right reflects the new data.

I am able to find the item like this:

handleDataChanged(data: Proposal){
  this.items$.subscribe((items: Proposal[]) => item = items.find(p => p.id 
  == data.id));
}

but cannot figure out how to update the item in the Observable, rather than just finding the one that changed.

I know I can 'trick' the component by navigating elsewhere and then back again to force it to refresh, but that also hits the API (and reloads the page as well).

The url looks like this:

/pages/proposals/manage/-XHzOJY/document

That slug in the url is the id of the currently selected item (which is rendered in the component on the right).

So I cannot use params change detection here, because it doesn't change. The user is causing a change to the already selected object, which is one of many inside the observable array.

UPDATE

Here is the full code for the parent component:

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, Subject } from 'rxjs/Rx';

import { Proposal } from '../proposal';
import { ProposalService } from '../proposal.service';
import { SearchService } from '../../services/search-service';

@Component({
  selector: 'fb-proposal-list',
  templateUrl: './proposal-list.component.html',
  styleUrls: ['./proposal-list.component.css']
})
export class ProposalListComponent implements OnInit {
  total$: Observable<number>;
  items$: Observable<Proposal[]>;

  term: string = "";
  currentPage: number = 1;
  private pageStream = new Subject<number>();  

  constructor(
    private _searchService: SearchService,
    private _proposalService: ProposalService,
    private _router: Router) {
  }

  ngOnInit() {
    this.setupSearching();
    // let timer = Observable.timer(0, 60000);
    // timer.subscribe(() => this.goToPage(this.currentPage));
  }

  setupSearching(){
    const searchSource = this._searchService.searchTermStream
      .map(searchTerm => {
        this.term = searchTerm;
        return {search: searchTerm, page: 1}
      });

    const pageSource = this.pageStream.map(pageNumber => {
      this.currentPage = pageNumber;
      return {search: this.term, page: pageNumber}
    });

    const source = pageSource
      .merge(searchSource)
      .startWith({search: this.term, page: this.currentPage})
      .switchMap((params: {search: string, page: number}) => {
        return this._proposalService.getProposalsPaged(params.search, params.page)
      })
      .share();

    this.total$ = source.pluck('meta').pluck('total_items');
    this.items$ = source.pluck('items'); 
  }

  goToPage(page: number) {
    this.pageStream.next(page)
  }  

  handleDataChanged(id: string){
    this.goToPage(this.currentPage);  
  }
}
like image 423
rmcsharry Avatar asked Mar 14 '17 23:03

rmcsharry


1 Answers

I don't think you fully understand how Observables work. What you call here "observable collection" is not what you might think it is, as in "collection of observable elements".

What it is, actually, is a stream of emitted collections. So when you have Observable<Model[]>, it's not a collection that's being observed by some observer, it's actually a stream of emitted Model[] collections. In that sense, you can't update the emitted value (obviously, as it already has been emitted), but what you want is it for observable to emit another, updated collection of Model.

Before you try that, you'll have to know that you can't emit something from observable that you haven't created yourself. What you need is a Subject, an object that is both Observable and Observer (it inherits from both Observer and Observable interfaces).

So, let's construct something like that:

subject: Subject<Proposal[]> = new Subject();
_proposals: Proposal[] = [];

get proposals() {
  return this.subject.asObservable();
}

Let's say you get your proposals from some API call:

this.http.get("...").map(response => response.json().subscribe(data => {
  this._proposals= <Proposal[]>data; // save your data
  this.subject.next(this._proposals); // emit your data
});

And now, you want to update your data:

updateProposals() {
  this._proposals = this._proposals.filter(...); // or whatever
  this.subject.next(Object.assign({}, this._proposals)); // emit completely new value
}

This may seem like a lot and I would recommend reading more on how Observables work for the sake of future issues that you might have. Cheers.

like image 97
Adnan A. Avatar answered Oct 15 '22 00:10

Adnan A.