67

I want to make navigation from child components that render inside router-outlet. My parent component have a router config and I want to navigate manually on some event. But I don't know how I can pass from child to parent some data (for navigation) without output. Because this construction is non working

 <router-outlet (navigateTo)="navigateToMessagePart($event)"></router-outlet>

How I can do it in right way? Maybe navigate it from child? But how I can get parent methods from child. Many thanks for any help!

Velidan
  • 5,526
  • 10
  • 48
  • 86

5 Answers5

153

<router-outlet></router-outlet> can't be used to emit an event from the child component. One way to communicate between two components is to use a common service.

Create a service

shared-service.ts

import { Observable } from "rxjs/Observable";
import { Injectable } from "@angular/core";
import { Subject } from "rxjs/Subject";

@Injectable()
export class SharedService {
    // Observable string sources
    private emitChangeSource = new Subject<any>();
    // Observable string streams
    changeEmitted$ = this.emitChangeSource.asObservable();
    // Service message commands
    emitChange(change: any) {
        this.emitChangeSource.next(change);
    }
}

Now inject the instance of the above service in the constructor of both the parent and child component.

The child component will be emitting a change every time the onClick() method is called

child.component.ts

import { Component } from "@angular/core";

@Component({
    templateUrl: "child.html",
    styleUrls: ["child.scss"]
})
export class ChildComponent {
    constructor(private _sharedService: SharedService) {}

    onClick() {
        this._sharedService.emitChange("Data from child");
    }
}

The parent component shall receive that change. To do so,capture the subscription inside the parent's constructor.

parent.component.ts

import { Component } from "@angular/core";

@Component({
    templateUrl: "parent.html",
    styleUrls: ["parent.scss"]
})
export class ParentComponent {
    constructor(private _sharedService: SharedService) {
        _sharedService.changeEmitted$.subscribe(text => {
            console.log(text);
        });
    }
}
starball
  • 20,030
  • 7
  • 43
  • 238
Antara Datta
  • 1,909
  • 2
  • 12
  • 16
  • Please could you show how you would then use 'text' on the parent component template? :) – Andrew Howard Mar 27 '17 at 20:07
  • @AndrewJuniorHoward, you could just assign it to a local variable. For example: export class ParentComponent { dummyText:string; constructor( private _sharedService: SharedService ) { _sharedService.changeEmitted$.subscribe( text => { this.dummyText = text; }); } } – Antara Datta Mar 28 '17 at 08:17
  • great answer, but i wanna highlight one small thing: if we do not unsubscribe changeEmitted$ after destroying component it would be emiting more times then one after we return in that component – dimson d May 24 '17 at 17:18
  • i tried this but i did't get any value ? . my value reached in service bit it did't enter in to the subcriber feild .can any one help me . – Ajmal Sha Jul 13 '17 at 09:37
  • @Ajmalsha same here. Did you find a solution? – Drunken Daddy Jul 20 '17 at 17:25
  • @HeisenBerg No . what about you – Ajmal Sha Jul 24 '17 at 08:52
  • 7
    yes, I did find the solution. You should add your service to providers only in the parent component. If you add it to both the parent and child components, a new instance of the service is injected into them both. Or if a want to single service to be shared among all the components in your application, add it to the providers in app.module.ts – Drunken Daddy Jul 25 '17 at 10:31
  • Can you use one shared-service for different component pairs or is it one shared-service/pair? – Milo Aug 25 '17 at 18:34
  • 1
    @Milo A shared service can be used by multiple components listening to it. It is not restricted to a pair. – Antara Datta Sep 04 '17 at 15:36
  • BUT how to share an instance of service between each pair of related components ? For instance I want to have a modal with a routed component inside an auxiliary route that will set a new value to be retrieved in the component of the primary outlet. It works, but i want to be able to use this for different components. If the service is provided globally then components will get a value that is not targeted to them. Should I use logic inside the service to filter by the recipients of the value ? – user2251745 Sep 29 '17 at 13:47
  • me sirvio de mucho tu ejemplo , muchas gracias. – Victor Grados Jan 05 '18 at 21:06
  • @HeisenBerg 'a new instance of the service is injected into them both' that explains soooo much in angular! Thanks, you're a god :D –  May 14 '18 at 19:51
  • The accepted answer show's you can use an event on router-outlet – Christopher Grigg Jan 17 '19 at 23:29
  • Excellent example of using a shared service. Thanks. – Nathan Beck Jan 24 '19 at 18:07
  • THIS SHOULD BE IN THE DOCS ( can't be used to emit an event from the child component.) – Usman Iqbal Feb 12 '19 at 07:47
  • @UsmanIqbal well, the fact that it doesn't tell you in the docs that you CAN do it, should tell you that it's not possible in the way you wish... – Emobe May 09 '19 at 10:35
  • This is a great working example. at least for me. thanks a ton man. – S M Iftakhairul Dec 08 '20 at 11:05
  • Creating a whole service is needed for a seemingly simple task like this? What if I need to communicate like this between several components? – JakeTheSnake Mar 18 '22 at 19:35
61

<router-outlet></router-outlet> is just a placeholder for adding routed components. There is no support for any kind of binding.

You can create a custom <router-outlet> that allows you to do that or more common, use a shared service to communicate between parent component and routed component.

For more details see https://angular.io/docs/ts/latest/cookbook/component-communication.html

update

There is now an event that allows to get the added component

<router-outlet (activate)="componentAdded($event)" (deactivate)="componentRemoved($event)"></router-outlet>

which allows to communicate (call getters, setters, and methods) with the component in componentAdded()

A shared service is the preferred way though.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
6

The answer given above is correct and complete. I just want to add for those who the solution didn't work for them that they should add the service to providers only in the parent component and not the child to ensure that you get a singleton of the service, otherwise two service instances will be created. This response is inspired by the comment of @HeisenBerg in the previous response.

Bacem
  • 181
  • 1
  • 10
  • this can now be solved with `providedIn: 'root'` added to the `Injectable` decorator. https://angular.io/guide/dependency-injection#create-an-injectable-service-class – Emobe May 08 '19 at 09:32
2

I changed a little from Antara Datta's answer. I created a Subscriber service

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

@Injectable({
  providedIn: 'root'
})
export class Subscriber<T>
{
  protected observable = new Subject<T>();

  public next(item: T)
  {
    this.observable.next(item);
  }

  public subscribe(callback: (item:T)=>void) {
    this.observable.subscribe(callback);
  }
}

Whenever I need two components to share some information, I inject this service in the constructor which subscribe to it:

 constructor(protected layoutOptions: Subscriber<Partial<LayoutOptions>>)
 {
    layoutOptions.subscribe(options => this.options = Object.assign({}, this.options, options));
 }

and the one which updates it

constructor(protected router: Router, protected apiService: ApiService, protected layoutOptions: Subscriber<Partial<LayoutOptions>>)
  {
    this.layoutOptions.next({showNavBar: false});
  }
Raza
  • 3,147
  • 2
  • 31
  • 35
1

It escapes my understanding why the router does not forward the "@Outputs".

I ended up dispatching barebones DOM events

// dom node needs to be a reference to a DOM node in your component instance
// preferably the root
dom.dispatchEvent(
      new CustomEvent('event', {
        detail: payload, // <- your payload here
        bubbles: true,
        composed: true,
      })
    );

You can catch it anywhere up the DOM tree like any other DOM event

Note: you need to unpack the payload from { detail: payload } on the receiving end..

Jan Kretschmer
  • 119
  • 1
  • 2