79

My app has a NameService which holds the name.

There are two child components of App, Navbar and TheContent which reference this service. Whenever the name changes in the service, i want it to update in both of the other components. How can i do this?

import {Component, Injectable} from 'angular2/core'

// Name Service

@Injectable()
class NameService {
  name: any;
  constructor() {
    this.name = "Jack";
  }
  change(){
    this.name = "Jane";
  }
}

// The navbar
@Component({
  selector: 'navbar',
  template: '<div>This is the navbar, user name is {{name}}.</div>'
})
export class Navbar {
  name: any;
  constructor(nameService: NameService) {
    this.name = nameService.name;
  }
}

// The content area
@Component({
  selector: 'thecontent',
  template: '<div>This is the content area. Hello user {{name}}. <button (click)=changeMyName()>Change the name</button></div>'
})
export class TheContent {

  name: any;

  constructor(public nameService: NameService) {
    this.name = nameService.name;
  }
  changeMyName() {
       this.nameService.change();
     console.log(this.nameService.name);
  }
}


@Component({
  selector: 'app',
  providers: [NameService],
  directives: [TheContent, Navbar],
  template: '<navbar></navbar><thecontent></thecontent>'
})
export class App {
  constructor(public nameService: NameService) {
  }
}
Francesco Borzi
  • 56,083
  • 47
  • 179
  • 252
B Hull
  • 3,153
  • 6
  • 22
  • 24

3 Answers3

113

Provide an event in the service and subscribe to it in the components:

@Injectable()
class NameService {
  name: any;
  // EventEmitter should not be used this way - only for `@Output()`s
  //nameChange: EventEmitter<string> = new EventEmitter<string>();
  nameChange: Subject<string> = new Subject<string>();
  constructor() {
    this.name = "Jack";
  }
  change(){
    this.name = 'Jane';
    this.nameChange.next(this.name);
  }
}
export class SomeComponent { 
  constructor(private nameService: NameService) {
    this.name = nameService.name;
    this._subscription = nameService.nameChange.subscribe((value) => { 
      this.name = value; 
    });
  }

  ngOnDestroy() {
   //prevent memory leak when component destroyed
    this._subscription.unsubscribe();
  }
}

See also
angular.io - COMPONENT INTERACTION - Parent and children communicate via a service

Theophilus Omoregbee
  • 2,463
  • 1
  • 22
  • 33
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    Also remember to unsubscribe from the event in the component else, the component instance will not be released. Need to call unsubscribe on the object returned by subscribe function. – Chandermani Jan 11 '16 at 07:32
  • @Chandermani Can you give ma a hint about how this is done in TS? – Günter Zöchbauer Jan 11 '16 at 07:36
  • 3
    `let subscription=nameService.nameChange.subscribe((value) => { this.name = value; }); subscription.unsubscribe()`. I have faced this issue hence the disclaimer. If subscription has to cancelled from other method, the variable should be class level – Chandermani Jan 11 '16 at 11:29
  • @Chandermani thanks a lot for the hint about unsubscribing and also about the TS guidance ;-) – Günter Zöchbauer Jan 11 '16 at 11:33
  • Yes @MarkRajcok i meant the same, instance variable. – Chandermani Jan 12 '16 at 03:10
  • @Chandermani, thanks, I just wanted to make sure I wasn't missing something before I edited the answer. – Mark Rajcok Jan 12 '16 at 03:14
  • `Günter Zöchbauer` : This approach or above answer of `hansmaad` works well. But In real world application when user refresh the page, will it work? I think no because nameService will get initialized again and again 'Jack' name will appear. Any solution from your end? – micronyks Feb 16 '16 at 11:49
  • 1
    @micronyks The question was not about keeping the value when reloaded IMHO. For this you need to persist the data to a server and reload it or at least use localstorage, to store it in the browser. – Günter Zöchbauer Feb 16 '16 at 11:54
  • Thanks. I have a question but soon gonna raise a new question once practically go through it. – micronyks Feb 16 '16 at 12:37
  • This answer should be selected. This is exactly what I needed. Thank you! – Simon Apr 08 '16 at 07:45
  • 1
    I've been try to implement this solution, but I get an error in the service that says `property 'emit' does not exist on type 'Subject'`. Am I missing something in the above solution? Is there a working Plunkr of this solution? – Steve Schrab May 13 '16 at 13:56
  • @SteveSchrab I had the same issue. It seems that the rxjs/Subject library has changed the method from emit to add. If you use add then everyting is working fine. – crebuh Jun 06 '16 at 07:39
  • 1
    It should be `next` instead of `emit`. I mixed it up with `EventEmitter` I guess. – Günter Zöchbauer Jun 06 '16 at 07:47
  • @GünterZöchbauer yes you are right, i also mixed it up it is next instead of add ;) – crebuh Jun 06 '16 at 07:48
  • @GünterZöchbauer Why is the ngOnDestroy necessary here ? – Scipion Jul 13 '16 at 08:59
  • 3
    @Scipion In general, if you subscribe imperatively you should unsibscribe imperatively as well. In this case the component that subscribes to this service would probably still execute the callback passed to `subscribe()` on name change, even when it is already destroyed because the subscription stays active until the component gets garbage collected, which probably won't happen until the subscription is cancelled. – Günter Zöchbauer Jul 16 '16 at 10:56
  • hmm! where did you implement ngOnDestroy? – Mohammad Kermani Jun 28 '17 at 12:49
  • @GünterZöchbauer did this `unsubscribe` will reflect in all other components also ? – k11k2 Jan 11 '19 at 13:13
  • @k11k2 not sure why you would think that. It only unsubscribes exactly this one subscription created with `subscribe(...)` a few lines above. – Günter Zöchbauer Jan 11 '19 at 13:45
  • @GünterZöchbauer in my case, it reflecting every where. – k11k2 Jan 11 '19 at 13:57
  • Sorry, can't imagine what that means without seeing the code. I'd suggest you create a new question. – Günter Zöchbauer Jan 11 '19 at 13:58
  • Can anyone explain to me why Angular has moved in this direction? I've recently returned to Angular, last time I used it was Angularjs. The best part of Angularjs IMO was the power of data binding, but I feel like as I'm using it now I have to constantly define inputs and outputs, manually emit changes everywhere they happen, unsubscribe to avoid memory leaks, etc. I used to just define something in a service or controller and it would update automatically everywhere. Things like .watch were a godsend. It's all gone now and unless I'm missing something big I feel like this is a step backwards. – ryanovas May 04 '19 at 00:32
  • @ryanovas AFAIK the power of Angularjs' databinding was the main reason they built a different Angular from scratch. It was inefficient and error-prone and circular dependencies could cause endess loops and it was difficult to debug when something went wrong. The new Angular gives more power to the developer but it requires a bit more boilerplate. – Günter Zöchbauer May 04 '19 at 07:41
23

Since name in NameService is a primitive type, you'll get different instance in the service and your components. When you change name in NameService, the component properties still have the initial value and the binding doesn't work as expected.

You should apply the angular1 "dot rule" here and bind to a reference type. Change NameService to store an object that contains the name.

export interface Info {
   name:string;
}

@Injectable()
class NameService {
  info: Info = { name : "Jack" };
  change(){
    this.info.name = "Jane";
  }
}

You can bind to this object and get updates to the name property automatically.

// The navbar
@Component({
  selector: 'navbar',
  template: '<div>This is the navbar, user name is {{info.name}}.</div>'
})
export class Navbar {
  info: Info;
  constructor(nameService: NameService) {
    this.info = nameService.info;
  }
}
hansmaad
  • 18,417
  • 9
  • 53
  • 94
  • This is the way I would do it too. In angular 1 "always have a dot in your model" is a good way to reduce the effort to sync your bindings, it may be appropriate in Angular2 as well, depending on the use case. Without it, using event emitters to sync the model can get tedious. – Michael Kang Jan 11 '16 at 08:24
  • wow, why does this work compared to the EventEmitter way? it feels like so much less work? – B Hull Jan 11 '16 at 08:36
  • Does it update Navbar view? Please look at here [plnkr](http://plnkr.co/edit/G4TwpyQs9QuIPB68XyCQ?p=preview). Once you click on the button how would it update navbar view? – micronyks Jan 11 '16 at 08:43
  • This is done through your suggested way. still its not updating Navbar view. According to your suggestion I guess object's name value will be changed but it will not be updated in view. Isn't it? Guide me if I'm wrong. Please temper my plnkr and answer me. – micronyks Jan 11 '16 at 08:45
  • 7
    @micronyks In angular2 you can have multiple service instances. You should not define `providers` on both components. This way each component has its own `NameService` http://plnkr.co/edit/nScZgT1hsIKHZ7Z5E6u3?p=preview – hansmaad Jan 11 '16 at 08:52
  • 3
    @BHull, it works because with an object, both the service and the component refer to the same one object... and they each have a _reference_ to that one object. With a primitive type (string, number, boolean), the component gets its own copy of the primitive value in the constructor. Any change you make to the primitive `name` property in the service is unrelated to any change you make to primitive `name` property in the component. – Mark Rajcok Jan 12 '16 at 02:45
  • 1
    "If you change `name` in `NameService` you've got a different instance in the service and your components." Even if you don't change the value, the component gets its own `name` primitive property in the constructor. The initial value of this new primitive property is the value of the `name` property in the service, at the time of the assignment. The point I'm trying to make is that with primitive types, you never have the same "instance". One with reference types can you get two (or more) things referencing the same instance. – Mark Rajcok Jan 12 '16 at 02:50
12

I think that the solution provided by Günter is the best one.

That said, you must be aware that Angular2 services are singleton that take place into a tree of injectors. This means that:

  • if you define your service at the application level (within the second parameter of the bootstrap method), the instance can be share by all elements (components and service).
  • if you define your service at the component level (within the providers attribute), the instance will be specific to the component and its sub components.

For more details of such aspect, you can have a look at the "Hierarchical Dependency Injection" doc: https://angular.io/docs/ts/latest/guide/hierarchical-dependency-injection.html

Hope it helps you, Thierry

Thierry Templier
  • 198,364
  • 44
  • 396
  • 360