Shared variable value not being 'shared' when updated

Viewed 153

I am building an Angular app that contains the following:

Very Important: 1 shared variable: element_id This variable is defined without a value and lives in a service called api.service.ts

api.service.ts: This service contains 3 shared functions: 1 (getMenus) for getting the menu content from an API call, a second (getData) for getting data from the same API call and a third (change_val) that is called from the menu items in another component and changes the value of the shared variable (element_id) when each menu item is clicked. When the value of element_id changes, the new value is passed to getData and is supposed to pull new data from the API call. This isnt happening.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  url = 'api address';
  element_id; //here is the shared variable
  constructor(private http: HttpClient) { }

  public getMenus(){
    console.log('This ' + this.element_id + ' comes from api.getMenus')
    return this.http.get(this.url);
  }
  public getData(element_id){
    console.log('This ' + element_id + ' comes from api.getData')
    return this.http.get(this.url);
  }
  public change_val(element_id){
    console.log('This ' + element_id + " comes from api.change_val");
    this.element_id = element_id;
  }
}  

2 components

nav.component.ts

  1. this contains the logic for constructing a menu from a REST API call
  2. this also contains a shred public function for changing the value of the shared variable (element_id):

    import { Component, OnInit } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { ApiService } from '../api.service';
    
    @Component(
    {
    selector: 'app-nav',
    templateUrl: './nav.component.html',
    styleUrls: ['./nav.component.css']
    })
    
    export class NavComponent implements OnInit {
    menus;
    constructor(private api: ApiService,){}
    
    ngOnInit(){
    this.api.getMenus().subscribe((data: any[]) => {
    this.menus = data;
    });
    }
    
    public change_val(element_id){
    console.log('This ' + element_id + " comes from api.change_val");
    this.api.element_id = element_id;
    
    }
    }
    

content.component.ts

  1. this contains the logic for displaying content from an API call based on the value of element_id:

    import { Component, OnInit } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { ApiService } from '../api.service';
    
    @Component({
      selector: 'app-content',
      templateUrl: './content.component.html',
      styleUrls: ['./content.component.css']
    })
    export class ContentComponent implements OnInit {
      content;
      constructor(private api: ApiService) { }
    
     ngOnInit(){
        this.api.getData(this.api.element_id).subscribe(res => {
        this.content = res[this.api.element_id];
        console.log("This " + this.api.element_id + " is coming from content getData (via 
        api)");
        console.log(res[this.api.element_id]) ;
      } 
      )};
      }
    

nav.component.html

<div *ngFor="let menu of menus; index as id">
    <ul>
        <li><button (click)="change_val(id)">{{menu['course-lesson-name']}}</button>   
    </ul>
</div>

content.component.html

<p>{{content['course-lesson-content']}}</p>

Problems:

The content shows if I value element_id from the start, but doesn't change when the new values are passed to getData. The element_id value does update as required and is being logged to the console, but I need the getData to run each time the button.

If I leave element_id without a value, the content doesnt show, but the element_values will still update when the buttons are clicked. For some reason, the updated value of element_id doesnt seem to be getting to getting from the nav component to the service and out to the content component, this should happen each time a button is clicked and the value of element_id updates.

3 Answers

To make things easier you should have in place one of the commonly used component interaction mechanisms: https://angular.io/guide/component-interaction

I like to use the BehaviorSubject in a common service, in your case it would be the ApiService. BehaviorSubject is a special kind of observable that can emit new values (with its next method) to all its subscribers.

This is the "new" ApiService where the interesting points are the selectedMenu variable of type BehaviorSubject, with its type and the initial value set, and the change_val method where you emit the new values of the selectedMenu variable when the user selects a menu option:

export class ApiService {
  url = 'api address';
  element_id; //here is the shared variable
  selectedMenu = new BehaviorSubject<any>({ name: 'None', id: 0 }); // **IMPORTANT**
  menus = [{name: 'Menu 1', id: 1}, {name: 'Menu 2', id: 2}];
  constructor(private http: HttpClient) { }

  public getMenus(){
    console.log('This ' + this.element_id + ' comes from api.getMenus');
    return of(this.menus);
    //return this.http.get(this.url);
  }
  /*
  public getData(element_id){
    console.log('This ' + element_id + ' comes from api.getData');
    //return this.http.get(this.url);
  }*/

  public change_val(element_id){
    this.element_id = element_id;
    let selMen = this.menus.filter(x => x.id == element_id)[0];
    this.selectedMenu.next(selMen);  // **IMPORTANT**
  }

In your NavComponent you call the ApiService's ´change_val´ method when the user selects a menu option:

  public change_val(element_id) {
    console.log("This " + element_id + " comes from api.change_val");
    this.api.change_val(element_id); // **IMPORTANT**
  }

The ContentComponent is the "observer" where you subscribe to the selectedMenu BehaviorSubject of the ApiService:

  ngOnInit() {
    this.api.selectedMenu.subscribe(res => {
      this.content = res;
    }); // **IMPORTANT**
  }

I made a Stackblitz based almost completely on your code: https://stackblitz.com/edit/angular-j2wfjp

Add the service to the providers list of your application module definition.

@NgModule({
...
providers: [ApiService],
...
})
export class AppModule {}

The call to GetData is happening only once in your code ngOnInit. You'll need to call it whenever the data changes to get new data as a subscribe to a http call only gets a value back once. e.g:

nav.component:

    public change_val(element_id){
      this.api.element_id = element_id;

      this.api.getData(this.api.element_id).subscribe(res => {
        this.api.content = data;
      });
    }

If you move the content variable to your service also you can use refer to it in your other component to read the value:

Service:

export class ApiService {
  url = 'api address';
  element_id;
  content;

Content template:

<p>{{api.content['course-lesson-content']}}</p>

Note you should actually expose the content variable from the service as e.g. a BehaviourSubject so you can subscribe to it from other component to get the latest value. This shows how the call needs to be made to update it at least though.

Related