Search code examples
angularrxjsobservableangular-services

Shared service used in two component


I have a service for working with the db, where I can get/add/delete products. I'm using this products.service.ts in 2 components since I need to get products in 2 places in my application,

Therefore I want to switch my service to observable/subject so I can subscribe to it and it will be synced in the 2 places.

The thing is that I'm struggling making that switch for 2 days now.. if anyone could help me with that I'll be grateful !

It's like I'm using a different product "state" in each component because when I'm adding a product on one component it's updating only this component and the second is still the "old" one.

Here is my products service for the moment.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Product } from '../products/product.model';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators'

const BASE_URL = 'http://localhost:8001/api/';

@Injectable({
  providedIn: 'root'
})
export class ProductsService {
  private model = '';
  constructor(private http: HttpClient) { }

  all(sellerId, token): Observable<Product[]> {
  this.model = 'products'
    return this.http.get(this.getUrlById(sellerId), {headers: {'Authorization' : `Bearer ${token}`}})
    .pipe(map((products:Product[]) => products))
  }

  create(product, token) {
    this.model = 'product'
    return this.http.post(this.getUrl(), product, {headers: {'Authorization' : `Bearer ${token}`}});
  }

  update(product, token) {
    this.model = 'product'
    return this.http.put(this.getUrlById(product.id), product, {headers: {'Authorization' : `Bearer ${token}`}});
  }

  delete(productId) {
    return this.http.delete(this.getUrlById(productId));
  }

  private getUrl() {
    return `${BASE_URL}${this.model}`;
  }

  private getUrlById(id) {
    return `${this.getUrl()}/${id}`;
  }
}

For the moment I added the all function as an observable, but nothing else I tried worked so I left it as it was before. I am new with rxjs and angular in general (always worked with React in my previous works)

Thank you infinitely for those who will reply !!

This is the products.component.ts

import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AppSettings } from '../../app.settings';
import { Settings } from '../../app.settings.model';
import { Product } from './product.model';
import { ProductsService } from '../../shared/products.services';
import { ProductDialogComponent } from './product-dialog/product-dialog.component';
import { AngularFirestore } from '@angular/fire/firestore';
import { Router } from '@angular/router';
import { CookieService } from 'ngx-cookie-service';

@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.scss'],
  encapsulation: ViewEncapsulation.None,
  providers: [ ProductsService ]  
})

export class ProductsComponent implements OnInit {
    public products: Product[] = null;
    public searchText: string;
    public page:any;
    public settings: Settings;
    public firestore: any;
    public token = this.cookieService.get('token')
    public id = this.cookieService.get('id')
    constructor(public appSettings:AppSettings, 
                public dialog: MatDialog,
                public productsService:ProductsService,
                public router:Router,
                firestore: AngularFirestore,
                private cookieService: CookieService){
        this.settings = this.appSettings.settings; 
        this.firestore = firestore;
    }

    ngOnInit() {
        if (this.token) {
            this.getProducts()
        }
    }

    public getProducts(): void {
        if(this.id) {
            this.productsService.all(this.id, this.token)
            .subscribe(products => this.products = products)
        }
    }

    public createNewVersion(product:Product) {
        if(this.token) {
            this.productsService.createNewVersion(product.id, this.token)
            .subscribe(result => {
                this.getProducts()
            })
        }
    }
    
    public deleteProduct(product:Product){
        console.log(product.id)
    //    this.productsService.delete(product.id);
    }

    public openProductDialog(product){
        let dialogRef = this.dialog.open(ProductDialogComponent, {
            data: product
        });
        dialogRef.afterClosed().subscribe(result => {
            if(result) {
                console.log('worked')
                this.getProducts()
            }
        })
    }

    public callModulePage(productId) {
        this.router.navigate(['/modules'], {state: {data: productId}})
    }

}

Here is the sidenav component that show the products

import { Component, OnInit, Input, Output, ViewEncapsulation, EventEmitter } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { ProductsService } from 'src/app/shared/products.services';
import { AppSettings } from '../../../../app.settings';
import { Settings } from '../../../../app.settings.model';
import { MenuService } from '../menu.service';
import { MatDialog } from '@angular/material/dialog';
import { ProductDialogComponent } from '../../../../pages/products/product-dialog/product-dialog.component';
import { Product } from '../../../../pages/products/product.model'
import { ModulesService } from '../../../../pages/modules/modules.service'
import { CookieService } from 'ngx-cookie-service'

@Component({
  selector: 'app-vertical-menu',
  templateUrl: './vertical-menu.component.html',
  styleUrls: ['./vertical-menu.component.scss'],
  encapsulation: ViewEncapsulation.None,
  providers: [ MenuService, ProductsService, ModulesService]
})
export class VerticalMenuComponent implements OnInit {
  public settings: Settings;
  public products: Product[] = null;
  public productID = null
  public modules = {};
  private token = this.cookieService.get('token')
  private id = this.cookieService.get('id')
  public isModuleOpen = {};
  constructor(public appSettings:AppSettings, public menuService:MenuService, public router:Router, public productsService:ProductsService, private cookieService: CookieService, private modulesService:ModulesService, public dialog: MatDialog) { 
    this.settings = this.appSettings.settings;
  }

  ngOnInit() {
    // this.parentMenu = this.menuItems.filter(item => item.parentId == this.menuParentId);  
    if(this.token) {
     this.refreshProducts()
    }    
  }

  redirectTo(uri, moduleId, moduleName, product) {
    this.router.navigateByUrl('/', {skipLocationChange: true}).then(() =>
    this.router.navigate([uri], {state: {data: {moduleId, moduleName,product}}}));
  }

  showModules(productId) {
    this.modulesService.getModules(productId)
      .subscribe(m => {
        this.modules[productId] = m
        if(this.modules[productId].length > 0) {
          if(this.isModuleOpen[productId]) {
            this.isModuleOpen[productId] = false
          }else {
            this.isModuleOpen[productId] = true
          }
        }
      })
  }
  
  public openProductDialog(product){
    let dialogRef = this.dialog.open(ProductDialogComponent, {
        data: product
    });
    dialogRef.afterClosed().subscribe(result => {
        if(result) {
          this.refreshProducts()
        }
    })
  }
  public refreshProducts(){
    this.productsService.all(this.id, this.token)
    .subscribe(res => this.products = res)
  }
 }

There is a button in both component that opens a add-product-dialog component, here the dialog component with the create function

import { Component, OnInit, Inject, EventEmitter, Output } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { FormGroup, FormBuilder, Validators, NgForm} from '@angular/forms';
import { Product } from '../product.model';
import { ProductsService } from '../../../shared/productss.services'
import { CookieService } from 'ngx-cookie-service';

@Component({
  selector: 'app-product-dialog',
  templateUrl: './product-dialog.component.html',
  styleUrls: ['./product-dialog.component.scss'],
})
export class ProductDialogComponent implements OnInit {
  public form:FormGroup;
  public passwordHide:boolean = true;
  public token= this.cookieService.get('token')
  constructor(public dialogRef: MatDialogRef<ProductDialogComponent>,
              @Inject(MAT_DIALOG_DATA) public product: Product,
              public fb: FormBuilder,
              private productsService: ProductsService,
              private cookieService: CookieService) {
    this.form = this.fb.group({
      id: null,
      name: [null],
      shortname:[null],
      description: [null],
      overview: [null],
    });
  }

  ngOnInit() {
    if(this.product){
      this.form.setValue(
        {
          id: this.product.id,
          name: this.product.name,
          shortname: this.product.shortname,
          description: this.product.description,
          overview: this.product.overview
        }
      );
    } 
    else{
      this.product = new Product();
    } 
  }

  addProduct(product) {
    if(this.product.id) {
      if(this.token) {
        this.productsService.update(product, this.token)
        .subscribe(result => {
          console.log(result)
        })
      }
    } else {
      if(this.token) {
      this.productsService.create(product, this.token)
      .subscribe(result => console.log(result));
      }
    }

  }

  close(): void {
    this.dialogRef.close();
  }

}

Solution

  • Let's see the logic of what you want to achieve. 2 components always seeing a fresh list of products even when the change was invoked from 1 of those components. You also want to suppress that functionality inside a single service.

    The problem with your implementation is that each component subscribes to a different Observable which is created every time the all() method is invoked. The method brings an observable that carries that fresh information of products on that time line and then ends. The other component has previously subscribed to another observable of another past timeline which returned and ended.

    You can however make both components constantly listen to your Subscription. Then when 1 component asks for a fresh list of products, you retrieve it with http but instead of returning a new Observable (which the other component will not know) you emit a fresh value with the same Subscription that both components listen to.

    That translates to the following

    export class ProductsService {
      private model = '';
    
      //keep that Subscription as a reference here
      getAllProdObserv :  BehaviorSubject<Product[]> = new BehaviorSubject<Product[]>([]); 
    
      constructor(private http: HttpClient) { }
    
      //When this method get's called it will create an observable and when that observable is ready to be opened it will emit the value by your public subject
      all(sellerId, token): Observable<Product[]> {
      this.model = 'products'
    
        this.http.get(this.getUrlById(sellerId), {headers: {'Authorization' : `Bearer ${token}`}})
        .pipe(map((products:Product[]) => products)).subscribe( (products) => this.getAllProdObserv.next(products));
    
      }
    

    That way your other 2 components will always be listening on your Subject and see the values that it emits.

    Then on SideNav

    export class VerticalMenuComponent implements OnInit {
      public settings: Settings;
      public products: Product[] = null;
      public productID = null
      public modules = {};
      private token = this.cookieService.get('token')
      private id = this.cookieService.get('id')
      public isModuleOpen = {};
      constructor(public appSettings:AppSettings, public menuService:MenuService, public router:Router, public productsService:ProductsService, private cookieService: CookieService, private modulesService:ModulesService, public dialog: MatDialog) { 
        this.settings = this.appSettings.settings;
      }
    
      ngOnInit() {
        //You subscribe on the same Subject that will always exist but each time will emit the value of a fresh list
        this.productsService.getAllProdObserv.subscribe(res => this.products = res);
      }
    

    And then on products.component.ts

    export class ProductsComponent implements OnInit {
        public products: Product[] = null;
        public searchText: string;
        public page:any;
        public settings: Settings;
        public firestore: any;
        public token = this.cookieService.get('token')
        public id = this.cookieService.get('id')
        constructor(public appSettings:AppSettings, 
                    public dialog: MatDialog,
                    public productsService:ProductsService,
                    public router:Router,
                    firestore: AngularFirestore,
                    private cookieService: CookieService){
            this.settings = this.appSettings.settings; 
            this.firestore = firestore;
        }
    
        ngOnInit() {
            //Again you subscribe on the same Subject that will always exist but each time will emit the value of a fresh list
            this.productsService.getAllProdObserv.subscribe(products => this.products = products)
        }
    

    So by now products.component.ts and sidenav.component.ts will always listen to a single observable (actually Subject) and every emitted event will be rendered to both components.

    Your service will emit a new fresh list of products each time the method all is called.

    Then on products.component.ts the getProducts() will just make your service to emit a new value of the fresh products that both components will continuously listen to.

    export class ProductsComponent implements OnInit {
    
        ngOnInit() {
           this.productsService.getAllProdObserv.subscribe(products => this.products = products)
        }
    
        public getProducts(): void {
            if(this.id) {
                this.productsService.all(this.id, this.token);
            }
        }
    
        public createNewVersion(product:Product) {
            if(this.token) {
                this.productsService.createNewVersion(product.id, this.token)
                .subscribe(result => {
                    this.getProducts()
                })
            }
        }
    
        public openProductDialog(product){
            let dialogRef = this.dialog.open(ProductDialogComponent, {
                data: product
            });
            dialogRef.afterClosed().subscribe(result => {
                if(result) {
                    console.log('worked')
                    this.getProducts()
                }
            })
        }
    }
    

    Also the same in your sideNav component

    export class VerticalMenuComponent implements OnInit {
      public settings: Settings;
      public products: Product[] = null;
      public productID = null
      public modules = {};
      private token = this.cookieService.get('token')
      private id = this.cookieService.get('id')
      public isModuleOpen = {};
      constructor(public appSettings:AppSettings, public menuService:MenuService, public router:Router, public productsService:ProductsService, private cookieService: CookieService, private modulesService:ModulesService, public dialog: MatDialog) { 
        this.settings = this.appSettings.settings;
      }
    
      ngOnInit() {
        // this.parentMenu = this.menuItems.filter(item => item.parentId == this.menuParentId);  
        if(this.token) {
         this.productsService.getAllProdObserv.subscribe(res => this.products = res);
        }    
      }
    
      redirectTo(uri, moduleId, moduleName, product) {
        this.router.navigateByUrl('/', {skipLocationChange: true}).then(() =>
        this.router.navigate([uri], {state: {data: {moduleId, moduleName,product}}}));
      }
    
      showModules(productId) {
        this.modulesService.getModules(productId)
          .subscribe(m => {
            this.modules[productId] = m
            if(this.modules[productId].length > 0) {
              if(this.isModuleOpen[productId]) {
                this.isModuleOpen[productId] = false
              }else {
                this.isModuleOpen[productId] = true
              }
            }
          })
      }
      
      public openProductDialog(product){
        let dialogRef = this.dialog.open(ProductDialogComponent, {
            data: product
        });
        dialogRef.afterClosed().subscribe(result => {
            if(result) {
              this.refreshProducts()
            }
        })
      }
      public refreshProducts(){
        this.productsService.all(this.id, this.token);
      }
     }
    

    Edit: Just to add some info for OPs that face this thread and see the long discussion under. This answer here is enough to solve the issue. The Issue that we faced under discussion found to be that the service was not registered as singleton across the whole application but was registered again for different modules and different components in providers tag. After fixing that the behavior was as expected.