简体   繁体   中英

Passing Values Between Non-Parent/Child Relationship Components in Angular 2

From my understanding, in Angular 2, if you want to pass values between unrelated components (ie, 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?

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!

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.

(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).

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM