Search code examples
angularrxjsrxjs-observables

Angular 11: trying to assign values to an object using combineLatest


I have been struggling with this all day long. I have a component with an interface called Invoice and I need to assign values to some of the fields so I can pass the invoice object to a service to save to database. I normally program more imperatively but trying to be more declarative in this new component.

in my component I have some streams that I am trying to combineLatest on and then I need to get values from the combined streams and assign:

private newInvoice = {} as Invoice;

invoiceBatch$ = this.invoiceBatchService.invoiceBatch$;
nextInvoiceNumber$ = this.invoiceBatchService.newInvoiceNumberAction$;

ngOnInit() {
     combineLatest([
      this.nextInvoiceNumber$,
      this.invoiceBatch$,
      this.userService.currentUser$]
    ).subscribe(res => {
      this.newInvoice.InvoiceBatchID = res[1].InvoiceBatchID;
      this.newInvoice.FiscYearPer = res[1].FiscYearPer;
      this.newInvoice.InvoiceNumber = res[0];
      this.newInvoice.CreatedByID = +res[2].ApplicationUserID;
      this.newInvoice.CreatedBy = res[2].UserName;
    })
  }


  createNextInvoice(){
    this.invoiceBatchService.getNextInvoiceNumber();
}

in my my template, the user clicks a button to add a new invoice and it fires the createNextInvoice() method. This method calls the service that goes out and gets the next invoice number.

Invoice Batch Service

  private newInvoiceNumberSubject = new Subject<string>();
  private invoiceNumberDataStore: { invoicenumber: string } = { invoicenumber: null};
  readonly newInvoiceNumberAction$ = this.newInvoiceNumberSubject.asObservable();

  public getNextInvoiceNumber() {
    return this.http.get<string>(this.baseUrl + `/batch/GetNextReferenceNumber/9`)
      .subscribe(
        data => {
          this.invoiceNumberDataStore.invoicenumber = data;
          this.newInvoiceNumberSubject.next(Object.assign({}, this.invoiceNumberDataStore).invoicenumber);
          console.log("NewInvoice: ", data);
        }, error => {
        }
      );
  }

My thought was that when the 'createNextInvoice()' is called, it gets the next invoice number and updates nextInvoiceNumber$. Shouldn't the combineLastest react to the change to the nextInvoiceNumber$? Is subscribibg wrong? I've tried many combinations with map, mergeMap, switchMap, etc.

The invoiceBatch$ and this.userService.currentUser$ are set once when the component is created So nextInvoiceNumber$ is the only thing that will be updated from user action.

Update

I've gotten closer. I can assign values to object but it's firing twice every time a new invoice is added. It's progress!

I made these changes in my component:

  newInvoice$ = combineLatest([
    this.nextInvoiceNumber$,
    this.invoiceBatch$,
    this.userService.currentUser$]
  );

  ngOnInit() {

    this.newInvoice$.
      subscribe(res => {
        this.newInvoice.fdInvoiceNumber = res[0];
        this.newInvoice.fdInvoiceBatchID = res[1].fdInvoiceBatchID;
        this.newInvoice.fdFiscYearPer = res[1].fdFiscYearPer;
        this.newInvoice.fdCreatedByID = +res[2].fdApplicationUserID;
        this.newInvoice.fdCreatedBy = res[2].fdUserName;

        this.invoiceService.CreateInvoice(this.newInvoice);
      });

Solution

  • Not really sure what's going on for you, but here's a super simplified version of your code. You can copy it and run it yourself. Runs just fine.

    Since you haven't really told us what you expect to have happen, it's sort of hard to debug much further than this.

    // Declare a global that gets sets in a subscribe block later
    let newInvoice = {};
    // Declare a subject that we can .next whenever to trigger a combine latest
    const nextInvoiceNumber$ = new Subject<string>();
    
    // combine latest with the gives subject and two other simple 1-emission
    // observables
    combineLatest([
      nextInvoiceNumber$,
      of({InvoiceBatchID: 1111, FiscYearPer: 1234}),
      of({ApplicationUserID: 4321, UserName: "Tommy"})
    ]).subscribe(
      ( [InvoiceNumber, 
        {InvoiceBatchID, FiscYearPer}, 
        {ApplicationUserID, UserName}
      ]) => {
        // Give the global declare earlier a new value
        newInvoice = {
          ...newInvoice,
          InvoiceNumber,
          InvoiceBatchID,
          FiscYearPer,
          CreatedByID: ApplicationUserID,
          CreatedBy: UserName
        };
        // Log the new value just to see what's up
        console.log(newInvoice);
      }
    );
      
    // getNextInvoiceNumber returns a subscption and not an InvoiceNumber
    // We could generate a random invoice number to mimic an api call, but 
    // we'll just pass one in for brevity
    function getNextInvoiceNumber(invoiceNumber) {
      // Pretend timer is an api call like this.http.get
      return timer(3000).subscribe(_ => {
        // Somewhre in the subscribe, call .next on a subject
        nextInvoiceNumber$.next(invoiceNumber);
      })
    }
    
    // Pretend we getNextInvoiceNumber 3 times with different invoices each time
    // Ignore the return value of getNextInvoiceNumber
    getNextInvoiceNumber("1872");
    getNextInvoiceNumber("1986");
    getNextInvoiceNumber("2010");
    

    Here's the output; as expected only the invoice number changes:

    { CreatedBy: "Tommy"
    , CreatedByID: 4321
    , FiscYearPer: 1234
    , InvoiceBatchID: 1111
    , InvoiceNumber: "1872"
    , __proto__: Object
    }
    { CreatedBy: "Tommy"
    , CreatedByID: 4321
    , FiscYearPer: 1234
    , InvoiceBatchID: 1111
    , InvoiceNumber: "1986"
    , __proto__: Object
    }
    { CreatedBy: "Tommy"
    , CreatedByID: 4321
    , FiscYearPer: 1234
    , InvoiceBatchID: 1111
    , InvoiceNumber: "2010"
    , __proto__: Object
    }