5

From my understanding, in Angular 2, if you want to pass values between unrelated components (i.e., components that don't share a route and thus don't share a parent-child relationship), you do so via a shared service.

So that's what I've set up in my Angular2 app. I am checking to see if a certain series of characters exist in a url and returning true if it does.

  isRoomRoute(routeUrl) {
      if ((routeUrl.includes('staff') || routeUrl.includes('contractors'))) {
          console.log('This url: ' + routeUrl + ' is a roomRoute');
          return true;
      } else {
          console.log('This url: ' + routeUrl + ' is NOT a room route');
          return false;
      }
  }

In the constructor of the root app.component, I'm subscribing to routing events:

constructor(private routeService: RouteService,
            private router: Router)  {
    this.router.events.subscribe((route) => {
    let routeUrl = route.url;
    this.routeService.sendRoute(routeUrl);
    this.routeService.isRoomRoute(routeUrl);
    });
}

... and then using those provided urls to check whether or not a url contains the specific string. This is evaluated every time the route changes.

So that's all working as expected.

However, I'm running into a problem in passing the result of that check to a different, non-related (non-parent-child) component.

Even though I'm using a shared service (routeService) in both the app.component and the un-related (room.component) component, what works in one doesn't work in the other. From my understanding, the "truthiness" of what's being checked here should be enough to return a true statement.

But in the secondary, unrelated component, I get an "undefined" error when I call the function, like this:

  isRoomRoute() {
       if (this.routeService.isRoomRoute(this.routeUrl)) {
           return true;
       }
     }

So this is where I'm stuck. Basically the evaluation as to whether a url contains a certain string has already happened. Now I just need to pass the boolean result of that check to the secondary, non-related component. How can I best do this in Angular 2?

Muirik
  • 6,049
  • 7
  • 58
  • 116
  • services are not only for unrelated components see https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service – shusson Jan 10 '17 at 23:31
  • I understand that, but this is not what I was saying. I was addressing a specific use case as it pertains to passing values between non-parent-child components. – Muirik Jan 10 '17 at 23:33
  • You can use services for anything, related or unrelated. In a small app it could even make sense to just have one service that handles all event emitting for the entire app. I get some may say nay. – Tim Consolazio Jan 10 '17 at 23:33
  • Okay, but again, this is unrelated to my question. I know you can use services for many things. I wasn't being exhaustive. I was saying that when it comes to passing info between un-related components, you use a service to do so. – Muirik Jan 10 '17 at 23:34
  • Actually, this is generally not the case. A shared service will function like a singleton. In other words, two components will share the same instance of the service. – Muirik Jan 10 '17 at 23:36
  • My bet would be, you're not getting a single instance of the service, typically you'd provide it from a module and inject it into components, not sure I see that here. (For the sake of detail, I'm not sure it's actually a "singleton", it's more like a "managed instance". RobotLegs, one of the first really usable UI frameworks that used injection heavily, had an explanation citing singletons as being problematic in terms of injection). – Tim Consolazio Jan 10 '17 at 23:48
  • In the end, every component is related via "app". If you set up an event emitter at that level you basically have a clearing house for events. I'd prefer a global service over that, but again depending on your need, a small app could work with that fine. – Tim Consolazio Jan 10 '17 at 23:51
  • Tim, I agree it's not actually a singleton. That's why I said it functions "like a singleton". And, in that sense, it can pose problems, because what you change in one component will factor down to another - even if those two components aren't actually talking to each other. They will still inherit values via the shared service. Change one and it ends up being reflected in another. I ran into this very thing a week ago. – Muirik Jan 10 '17 at 23:54
  • Tim, also, these two components are being declared in the same root-level app.module. And it's in that same root-level app-module that the routeService is being provided. So, based on that, this should be a shared instance between the components, from what I understand. – Muirik Jan 10 '17 at 23:56
  • Well, that depends how you're providing/injecting it. Sure they may all be in the same module, but that doesn't mean you can't create new instances of them. As for the shared service, the point of sharing it would indeed be so that components could share and alter each other's data; you'd have to take that into account (different objects for different components all shared on the same model/service). On the other hand, if you just use in/outs and bind to one another's EventEmitters, none of that is a problem. I'm running out of space but I think I have a sample that could work for you. – Tim Consolazio Jan 10 '17 at 23:58
  • So, in your understanding, what would it look like to create new instances of them based on the scenario I just described? – Muirik Jan 10 '17 at 23:58
  • In both components, the shared service is being used like this: constructor(private sharedService: SharedService) {} – Muirik Jan 11 '17 at 00:01
  • Time, by the way, a sample would be great! – Muirik Jan 11 '17 at 00:02
  • How is it being provided from the module (and is it injectable etc.) I'm working on the EventEmitter naive sample gimme a few mins. – Tim Consolazio Jan 11 '17 at 00:02
  • The routeService has the @Injectable decorator, yes. – Muirik Jan 11 '17 at 00:05

4 Answers4

6

Your understanding is correct, an injectable shared service is a common way of communication between multiple, unrelated components.

Here is the walk-through of such a use case.

Firstly, suiting your situation, we will listen the Router events in AppComponent, obtain the active route, and pass it to RouteService so the service can manipulate it, and/or serve it to other components.

This is how the AppComponent should look like:

export class AppComponent {

    constructor(private _router: Router,
                private _routeService: RouteService) {

        this._router.events.subscribe(event => {
            if (event instanceof NavigationEnd) {
                let url = event.urlAfterRedirects;
                this._routeService.onActiveRouteChanged(url);
            }
        });
    }

}

When it comes to the service, here we'll introduce the BehaviorSubject as a delegate, so the components using the service can subscribe to a service data changes. For more information about BehaviorSubject and other Subjects, visit: Delegation: EventEmitter or Observable in Angular2

Here is the implementation of our shared RouteService (components need to use the single instance of the service, so make sure you've provided it at the root level):

@Injectable()
export class RouteService {

    isRoomRouteSource: BehaviorSubject<boolean> = new BehaviorSubject(false);

    constructor() { }

    onActiveRouteChanged(url: string): void {
        let isRoomRoute = this._isRoomRoute(url);
        this.isRoomRouteSource.next(isRoomRoute);
        // do other stuff needed when route changes
    }

    private _isRoomRoute(url: string): boolean {
        return url.includes('staff') || url.includes('contractors');
    }
}

The example of another component using the service, and subscribing to our BehaviorSubject changes:

export class AnotherComponent {

    isCurrentRouteRoomRoute: boolean;

    constructor(private _routeService: RouteService) {
        this._routeService.isRoomRouteSource.subscribe((isRoomRoute: boolean) => {
            this.isCurrentRouteRoomRoute = isRoomRoute;
            // prints whenever active route changes
            console.log('Current route is room route: ', isRoomRoute);
        });
     }

}

If subscribing to isRoomRouteSource changes isn't necessary, say we just need the last value stored, then:

export class AnotherComponent {

    isCurrentRouteRoomRoute: boolean;

    constructor(private _routeService: RouteService) {
        this.isCurrentRouteRoomRoute = this._routeService.isRoomRouteSource.getValue(); // returns last value stored
        console.log('Current route is room route: ', this.isCurrentRouteRoomRoute);
     }

}

Hope this helped!

Community
  • 1
  • 1
seidme
  • 12,543
  • 5
  • 36
  • 40
1

Just looking at your code it looks like something is incorrect here.

  isRoomRoute() {
       if (this.routeService.isRoomRoute(this.routeUrl)) {
           return true;
       }
     }
It looks to me as if this.routeUrl in the above code will likely be undefined unless it is defined elsewhere and defined before . What you could do is instead set a property in the service on the route event and then in the isRoomRoute you would read that property.

@Injectable()
class routeService {
  constructor(private router: Router) {
    // subscribe to event
    router.subscribe((url) => {
      this.routeUrl = url;
      // other things?  sendRoute??
    });

  }

  // Other methods for this class
  isRoomRoute() {
    return this.routeUrl && (this.routeUrl.includes('staff') || this.routeUrl.includes('contractors'));
  }
}

// Usage later where this service has been injected
@Component({
 // ... other properties
 providers: [routeService]
})
class someComponent {
  constructor(private routeService: routeService) {}
  someMethod() {
    this.routeService.isRoomRoute();  // Check if we are in a room route.
  }
}

In a case like this, I am not sure why you can't simply get the URL and parse it when isRoomRoute called instead of setting something on routing events.

Goblinlord
  • 3,290
  • 1
  • 20
  • 24
  • Yes, the issue is definitely that "this.routeUrl" is undefined when I call it in room.component. That's still strange to me, considering I can console log the correct value for routeUrl in that same component. Question: in your answer, what does 'someStr' represent? – Muirik Jan 12 '17 at 05:11
  • Actually, the condition would be: `(routeUrl.includes('staff') || routeUrl.includes('contractors')` based on your stuff above. Just changed in my answer. – Goblinlord Jan 12 '17 at 07:15
  • As for why `this.routeUrl` is undefined I just didn't see it defined anywhere in your code you showed. I would need more to say exactly why. The other component has nothing that I see that does `this.routeUrl = something` so it would be undefined. You would also have to worry about js `this` semantics wherever and however you are calling something which will reference `this`. – Goblinlord Jan 12 '17 at 07:28
0

(Was asked to post a sample of what I was talking about in comments):

I think of practical Angular events/dataflow this way:

  • "Interested" hears emitted event from a component's EventEmitter (because it has a reference to it, and is subscribed to that reference).
  • Something emits an even via an EventEmitter, and anything with reference to it and subscribed to it, will hear it.

They all do it with EventEmitters. So a parent can hook into a child's event emitter, and a child can hook into a parent's. Everybody can hook into the Service's. But also, everything is a child of "app". So in the end (although a Service would be the way to go), you can build a complete component comm system just by having every component hook into "app"'s event emitter.

This sample is one way to approach the menubar button comm problem: when one button is clicked, you want them all to know it (so you can highlight the selected background and unhighlight the rest, whatever). The button components are peers, related only because they have a higher level parent.

So in the parent component (which can be as confined as a MenuBarComponent, or as broad as "app"):

<ul>
  <li *ngFor="let btn of buttons">
    // Bind the parent's broadcast listener, to the child Input. 
    <my-child [broadcastListener]="broadcasterParent">
    </my-child>
  </li>
</ul>

Here the parent is giving the child, via an Input, a reference to its (the parent's) EventEmitter (broadcast and such are just typical names). So when the parent emits an event from its EventEmitter, or any of the children use the reference to that emitter to emit an event, all the components with a reference via an Input, and that have subscribed to that reference, will hear it.

The parent code behind that template above (note I use ES6, pretty sure you'll get the idea, and I just use constructors to keep it short):

import { Component, EventEmitter } from '@angular/core';
...
constructor  ) {
  // this is the instance given to the child as Input
  this.broadcasterParent = new EventEmitter ( );
}

In the child:

import { Component } from '@angular/core';
...
  constructor ( ) {
    // This is almost certainly better used in OnInit.
    // It is how the child subscribes to the parent's event emitter.
    this.broadcastListener.subscribe( ( b ) => this.onBroadcast ( b ) );
  }

  onButtonClick ( evt ) {
    // Any component anywhere with a ref to this emitter will hear it
    this.broadcastListener.emit ( evt );
  }

  onBroadcast ( evt ) {
    // This will be the data that was sent by whatever button was clicked
    // via the subscription to the Input event emitter (parent, app, etc).
    console.log ( evt );        
}

ChildComponent.annotations = [
  new Component ( {
      ... // TS would use an attribute for this
      inputs: [ 'broadcastListener' ]
      template: `
        <div click)="onButtonClick ( $event )">
           ...
        </div>
      `
  })
];

A service really does more or less the same thing, but a service is "floating" and access via injection, as opposed to fixed in the hieararchy and accessing via Input (and you can make it more robust etc).

So any one button that is clicked, will emit an event which they will all hear because they are all subscribed to the same event emitter (be it in a Service or whatever).

Tim Consolazio
  • 4,802
  • 2
  • 19
  • 28
  • Thanks, Tim. I will look through this to apply to my situation. – Muirik Jan 11 '17 at 03:30
  • I'm still unclear how this would work when the two components don't have a parent-child relationship. Bottom line, I need to either pass the value of "routeUrl" to the non-child component, so it can evaluate itself whether or not that route matches certain parameters... Or, I need to just pass on the boolean result that was found in the service, on to the non-child component. – Muirik Jan 11 '17 at 19:34
  • So how would I pass on a boolean result from a service to component calling that service? – Muirik Jan 11 '17 at 19:36
  • Some component somewhere, that is subscribed to the EventEmitter of something else (in your case, a service), uses the Service's EventEmitter (or a Service function wrapping it), to emit an event: mySvc.emitEvent ( someValue ), similar to the buttonClick above. Any other component that has the Service injected into it and that has subscribed to that emitter, will hear that emitted event and receive the data from the originator. There are no parent child relationships here. Just two components with the same injected service that manipulates event emitter. – Tim Consolazio Jan 11 '17 at 20:10
  • I'll have some time later if you still need to create a CLI project that illustrates this, I'll do something like create a couple of components that appear unrelated by hierarchy (even though everything is a child of "app" ultimately), and use a service to pass around data via emitted events. Again, the point of what I show above is, it doesn't matter how the component gets the reference to the event emitter; it can get it from a service, it can get it via an Input. After they the usage is the same. – Tim Consolazio Jan 11 '17 at 21:04
0

Said I'd do it, so here's the same idea as my first answer, just using a "floating" service (so no parent/child thing). Although naive, this is pretty much the crux of it.

First, create the service EmitService.

import { Injectable, EventEmitter } from '@angular/core';

@Injectable()
export class EmitService {

  private _emitter;

  constructor() {
    this._emitter = new EventEmitter ( );
  }

  subscribeToServiceEmitter ( callback ) {
    return this._emitter.subscribe ( b => callback ( b ) );
  }

  emitThisData ( data ) {
    this._emitter.emit ( data );
  }
}

Create two components, they can be anywhere in the app. Here's CompOneComponent, copy it to create CompTwoComponent:

import { Component, OnInit, OnDestroy } from '@angular/core';
// the CLI puts components in their own folders, adjust this import
// depending on your app structure...
import { EmitService } from '../emit.service';

@Component({
  selector: 'app-comp-one',
  templateUrl: './comp-one.component.html',
  styleUrls: ['./comp-one.component.css']
})
export class CompOneComponent implements OnInit, OnDestroy {

  private _sub;

  constructor( private _emitSvc : EmitService ) {}

  ngOnInit() {
    // A subscription returns an object you can use to unsubscribe
    this._sub = this._emitSvc.subscribeToServiceEmitter ( this.onEmittedEvent );

    // Watch the browser, you'll see the traces. 
    // Give CompTwoComponent a different interval, like 5000
    setTimeout( () => {
      this._emitSvc.emitThisData ( 'DATA FROM COMPONENT_1' );
    }, 2000 );

  }

  onEmittedEvent ( data ) {
    console.log ( `Component One Received ${data}` );
  };

  ngOnDestroy ( ) {
    // Clean up or the emitter's callback reference
    this._sub.unsubscribe ( );
  }
}

Add it all to your app; the components are all top level here, but they don't have to be, they can exist anywhere:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { CompOneComponent } from './comp-one/comp-one.component';
import { CompTwoComponent } from './comp-two/comp-two.component';

import { EmitService } from './emit.service';

@NgModule({
  declarations: [
    AppComponent,
    CompOneComponent,
    CompTwoComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  exports: [ ],
  providers: [ EmitService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

And that's it. Now both components have access to the Service (and have subscribed to its EventEmitter), so they can tell it to emit events, and will receive whatever events are fired by whatever other component. Create a CLI app, add this stuff, run it, and you'll see the console.logs fire as you'd expect (note the component that emits the event will also hear it, you can filter that out a couple of different ways). Anything that you can inject the EmitService into can use it regardless of "where" it is.

Tim Consolazio
  • 4,802
  • 2
  • 19
  • 28