Home > Software design >  Angular 13: subscription to service, update a variable on changes?
Angular 13: subscription to service, update a variable on changes?

Time:07-08

I have a service, to which I would like to pass a string from any component in my app.

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable()
export class TestService {

  private messageSource = new BehaviorSubject<string>('');
  currentMessage = this.messageSource.asObservable();

  constructor() {}

  changeMessage(message) {
    this.messageSource.next(message);
  
  }
}

in my child component, I update it:

  constructor(private ts: TestService) {}

  onTestMe() {
    this.ts.changeMessage('foobar');
  }

in another (in this case, parent) component, I subscribe to it:

import { Component, OnInit } from '@angular/core';
import { TestService } from './test.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  message: string;

  constructor(private ts: TestService) {}

  ngOnInit(): void {
    this.ts.currentMessage.subscribe((message) => (this.message = message));
    console.log('message on root page: ', this.message);
  }
}

in the root HTML, I have a binding {{message}}

The binding is updating but how do I update the value of the message variable in the TS whenever it's changed?

In other words, I need to know the value of message to use in other functions in the TS and I need to be sure it's current.

See stackblitz of above: https://stackblitz.com/edit/angular-ivy-6u7ueb?file=src/app/hello.component.html

CodePudding user response:

The console.log(...) in ngOnInit() will have no effect.

how do I update the value of the message variable in the TS whenever it's changed?

this line of code should update the value of message:

this.ts.currentMessage.subscribe((message) => (this.message = message));

I need to know the value of message to use in other functions in the TS and I need to be sure it's current.

Can you give an example of this usage? Like, you have a different function in AppComponent which also uses message, but how do you want to ensure that the value is current? What do you mean by "it is current"?

CodePudding user response:

Instead of this:

export class AppComponent implements OnInit {
  // This will always be updated with the emitted value
  message: string;

  constructor(private ts: TestService) {}

  ngOnInit(): void {
    this.ts.currentMessage.subscribe((message) => (this.message = message));
    // This code only executes once, so will only log the original value
    console.log('message on root page: ', this.message);
  }

  onOpenAlert() {
    window.alert('The message is '   this.message);
  }
}

Try this:

export class AppComponent implements OnInit {
  // This will always be updated with the emitted value
  message: string;

  constructor(private ts: TestService) {}

  ngOnInit(): void {
    this.ts.currentMessage.subscribe((message) => {
      this.message = message;
      // Now this executes on each emission
      console.log('message on root page: ', this.message);
     });
  }

  onOpenAlert() {
    window.alert('The message is '   this.message);
  }
}

Notice that the logging is now within the subscribe so it will execute each time a new value is emitted.

In either case, the message variable is being updated on each emission.

CodePudding user response:

I have two options on how to use service from child to parent or otherwise.

First, we can use pipe async for the data of service, because the advantage use pipe async is auto unsubsribe when the component has retrieves data.

For app.component.ts

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { TestService } from './test.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  $message: Observable<string> = this.ts.currentMessage;

  constructor(private ts: TestService) {}

  ngOnInit(): void {
    // this.ts.currentMessage.subscribe((message) => (this.message = message));
    // console.log('message on root page: ', this.message);
  }

  onOpenAlert() {
   // window.alert('The message is '   this.$message);
  }
}

For app.component.html

<h1>Parent</h1>
<p>Message: {{ $message | async }}</p>

<button (click)="onOpenAlert()">Alert Me</button>
<hr />

<hello></hello>

The second option is manually to unsubscribe services on the life cycle of angular which means on ngOnDestroy, the code becomes like this.

For app.component.ts

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { TestService } from './test.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit, OnDestroy {
  $message: Subscription;
  message: string;

  constructor(private ts: TestService) {}
  ngOnDestroy(): void {
    if (this.$message) this.$message.unsubscribe();
  }

  ngOnInit(): void {
    this.$message = this.ts.currentMessage.subscribe((message) => {
      console.log('message on root page: ', this.message);
      this.message = message;
    });
  }

  onOpenAlert() {
    window.alert('The message is '   this.message);
  }
}

For app.component.html

<h1>Parent</h1>
<p>Message: {{ message }}</p>

<button (click)="onOpenAlert()">Alert Me</button>
<hr />

<hello></hello>

More detail information you can visit this documentation https://angular.io/guide/observables

CodePudding user response:

Instead of copying values from observable emissions to a separate local variable, keep your message as an observable which can then be unwrapped by the template using the async pipe.

In your component:

  • keep the message as an observable (don't subscribe!)
  • add a message argument to your onOpenAlert() method
message$ = this.ts.currentMessage;

onOpenAlert(message: string) {
  window.alert('The message is '   message);
}

In your template:

  • utilize the async pipe to handle the subscribing
  • pass the unwrapped 'message' value into your function
<div *ngIf="message$ | async as message">

  <h1>Parent</h1>
  <p>Message: {{ message }}</p>

  <button (click)="onOpenAlert(message)">Alert Me</button>
  <hr />

</div>

Here's a working StackBlitz demo.


To make this work with minimal changes, I did a dirty little thing in that I set your default value emitted by your BehaviorSubject from empty string to a single space. The reason for this is that the *ngIf="message$ | async" would evaluate to false when the message is an empty string and not render the element. This hack may not be acceptable. The typical way to handle this is to always have your observable emit a "view model" object that will never be null. This is explained in this answer.

  • Related