0

I am having trouble with .subscribe() in angular, where the values I use to update the UI are not themselves updating. In my Inspect Element Network tab, I can see that the data is correct. And before anyone mentions it, the nested .subscribe() is not the problem, the first subscribe is always true, as the user must be logged in to view this screen.

ngOnInit(): void {
    this.service.isLoggedIn().subscribe( u => {
        this.user = u;
        this.id = this.location.selected.value.id
        this.location.getSelected().subscribe( selectedLocation => {
          if (selectedLocation) {
            this.showTable = true;
            if (selectedLocation.members) {
              this.member = true;
            }
            if (selectedLocation.schedules) {
              this.schedules = true;
            }
            if (selectedLocation.team) {
              this.team = true;
            }
          }
        });
    });
getSelected(): Observable<Location> {
    if (this.selected && this.selected.value) {
      return of(this.selected.value);
    } else {
      return this.selected.asObservable();
    }
  }

This is what is shown in my Network tab in Inspect Element:

{
    "id": 4,
    "member": true,
    "schedules": true,
    "team": false
}

However, when I go to this page, for example, the if(selectedLocation) is triggered, but the other if()s are not, and thus my variables are not changed, therefore neither is the UI.

LondonMassive
  • 367
  • 2
  • 5
  • 20
  • If the requests are asynchronous you might need to trigger change detection yourself. – martin Jul 16 '21 at 12:15
  • **1.** The nested subscription could be replaced with a `forkJoin` **2.** And this has to do with the error: How do you get the response in the Network of the browser console when the function `getSelected()` you've shown clearly returns either a `BehaviorSubject` or it's `.value` and not an HTTP request? – ruth Jul 16 '21 at 12:16
  • @martin hi, thanks for the reply, I have seen people mention change detection before, with ``markForCheck()`` and ``detectChanges()`` but with little to no explanation of how to use them, or how they work. – LondonMassive Jul 16 '21 at 12:46
  • @MichaelD I am not entirely sure about your second point, I have just left university and started working at a new company, I didn't build this project, I have just been asked to fix this bug. – LondonMassive Jul 16 '21 at 12:49
  • https://stackoverflow.com/questions/34827334/triggering-change-detection-manually-in-angular – martin Jul 16 '21 at 13:32
  • I have tried this approach, and it had no effect on the outcome – LondonMassive Jul 16 '21 at 13:35
  • @martin doesn't ``markForCheck()`` only update the UI components? My issue is regarding the fact that my ``.subscribe()`` is not seeing the updated data – LondonMassive Jul 16 '21 at 13:39
  • Is this related (duplicate) of this question: https://stackoverflow.com/questions/68394743/angular-subscribe-not-updating – DeborahK Jul 16 '21 at 18:30
  • I've started a Stackblitz for you here: https://stackblitz.com/edit/angular-posts-behavior-subject-procedural-vs-declarative-zsbs9e so we could get enough of this running to help you resolve the issue. However, I don't know what some of the above code is, such as how `this.selected` or `this.location` are defined. – DeborahK Jul 16 '21 at 19:00
  • Are you resetting the boolean values back to false anywhere as this code only sets them to true. – DeborahK Jul 16 '21 at 19:33

2 Answers2

0

I don't think we can see enough here to really get at what the problem is. If it's about angular's change detection, the following may help.

// Angular will perform this dependency injection for you
constructor(private ngZone: NgZone) { }

/*****
 * Wrap every emission of an observable (next, complete, & error)
 * With a callback function, effectively removing the invocation
 * of these emissions from the observable to the callback.
 *
 * This isn't really too useful except for as a helper function for
 * our NgZone Operators where we leverage this wrapper to run an
 * observable in a specific JavaScript environment.
 *****/
callBackWrapperObservable<T>(
  input$: Observable<T>, 
  callback: (fn: (v) => void) => void
): Observable<T> {
  const callBackBind = fn => (v = undefined) => callback(() => fn(v));
  const bindOn = (ob, tag) => callBackBind(ob[tag].bind(ob));
  return new Observable<T>(observer => {
    const sub = input$.subscribe({
      next: bindOn(observer, "next"),
      error: bindOn(observer, "error"),
      complete: bindOn(observer, "complete")
    });
    return { unsubscribe: () => sub.unsubscribe() };
  });
}

/*****
 * If we've left the angular zone, we can use this to re-enter
 * 
 * If a third party library returns a promise/observable, we may no longer be in
 * the angular zone (This is the case for the Google API), so now we can convert such
 * observables into ones which re-enter the angular zone
 *****/
ngZoneEnterObservable<T>(input$: Observable<T>): Observable<T> {
  return this.callBackWrapperObservable(input$, this.ngZone.run.bind(this.ngZone));
}

/*****
 * This is a pipeable version of ngZoneEnterObservable
 *****/
ngZoneEnter<T>(): MonoTypeOperatorFunction<T> {
  return this.ngZoneEnterObservable;
}

/*****
 * Any actions performed on the output of this observable will not trigger 
 * angular change detection. 
 *****/
ngZoneLeaveObservable<T>(input$: Observable<T>): Observable<T> {
  return this.callBackWrapperObservable(input$, this.ngZone.runOutsideAngular.bind(this.ngZone));
}

/*****
 * Pipeable version of ngZoneLeaveObservable
 *****/
ngZoneLeave<T>(): MonoTypeOperatorFunction<T> {
  return this.ngZoneLeaveObservable;
}

// How I would use this to try to solve your problem.
ngOnInit(): void {
  this.service.isLoggedIn().pipe(

    mergeMap(u => {
      this.user = u;
      // The logic of the following line makes no sense to me, 
      // you're assuming selected has a value and then right after
      // you're calling a function that explicitly doesn't assume this.
      this.id = this.location.selected.value.id;
      return this.location.getSelected();
    }),

    ngZoneEnter()

  ).subscribe(selectedLocation => {

    if (selectedLocation) {
      this.showTable = true;
      if (selectedLocation.members) {
        this.member = true;
      }
      if (selectedLocation.schedules) {
        this.schedules = true;
      }
      if (selectedLocation.team) {
        this.team = true;
      }
    }
    
  });
}
Mrk Sef
  • 7,557
  • 1
  • 9
  • 21
  • What do you mean by I am assuming that selected has a value, then right after calling a function that assumes it does not? – LondonMassive Jul 16 '21 at 13:55
  • Look at the code you gave us for this.location.getSelected. It has an if-else branch for whether the value has been set yet (Which is silly anyway since the BehaviourSubject will return that value once it arrives either way, but hey). – Mrk Sef Jul 16 '21 at 14:10
0

I put together a stackblitz for you here: https://stackblitz.com/edit/angular-posts-behavior-subject-procedural-vs-declarative-zsbs9e

Hopefully it is close enough to represent what you are trying to achieve.

Here is the key pieces of code:

  // Stream that emits the logged in user
  loggedInUser$ = this.isLoggedIn();

  // Stream that emits each time the user selects a different location
  locationChangedSubject = new BehaviorSubject<number>(0);
  locationChanged$ = this.locationChangedSubject.asObservable();

  // Combined stream to ensure both streams emit before processing
  selectedLocation$ = combineLatest([
    this.loggedInUser$,
    this.locationChanged$
  ]).pipe(
    switchMap(([user, locationId]) =>
      this.getSelected(locationId).pipe(
        tap(selectedLocation => {
          this.showTable = Boolean(selectedLocation);
          this.member = Boolean(selectedLocation?.members);
          this.schedules = Boolean(selectedLocation?.schedules);
          this.team = Boolean(selectedLocation?.team);
        })
      )
    )
  );

A few notes:

  1. I created individual declared properties for each of the streams. This gives you more flexibility, makes it easier to subscribe/unsubscribe (if you don't use an async pipe) or bind directly to the property (if you do use an async pipe).

  2. I used a combineLatest to combine the streams instead of using nested subscribes. (Nested subscribes are not a recommended technique.)

  3. I modified the set of if conditions to set true OR false. Currently it appeared you were only ever setting the values to true, never to false to clear them.

  4. Since the switchMap is a higher order mapping operator, it automatically subscribes to it's inner observable (the one returned from the this.getSelected().

DeborahK
  • 57,520
  • 12
  • 104
  • 129