29

I'm trying to get a button click in one component to put focus on an element on another component. (Frankly, I don't understand why this must be so complex, but I have not been able to implement any simpler way that actually works.)

I'm using a service. It doesn't need to pass any data except that the click occurred. I'm not sure how the listening component is meant to respond to the event.

app.component:

Skip to main content

import { Component } from '@angular/core';
import { SkipToContentService } from './services/skip-to-content.service';


export class AppComponent {
    constructor(
        private skipToContent: SkipToContentService
    ) {}
    }

    skipLink() {
        this.skipToContent.setClicked();
    }

}

login component:

<input type="text" name="username" />


import { Component, OnInit } from '@angular/core';
import { SkipToContentService } from '../../../services/skip-to-content.service';

export class BaseLoginComponent implements OnInit {

    constructor(
        private skipToContent: SkipToContentService
        ) {
    }

    ngOnInit() {
        this.skipToContent.skipClicked.subscribe(
            console.log("!")
            // should put focus() on input
        );

    }
}

skip-to-content.service:

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

@Injectable()
export class SkipToContentService {

    skipClicked: EventEmitter<boolean> = new EventEmitter();


    constructor() {
    }

    setClicked() {
        console.log('clicked');
        this.skipClicked.emit();
    };
}

I'm a bit lost here as to how logon will "hear" the skipClicked event.

DaveC426913
  • 2,012
  • 6
  • 35
  • 63
  • 2
    You shouldn't use `EventEmitter` in your services https://stackoverflow.com/questions/36076700/what-is-the-proper-use-of-an-eventemitter. And use a shared service for broadcasting events: https://angular.io/guide/component-interaction#parent-and-children-communicate-via-a-service – eko Aug 02 '17 at 07:30

2 Answers2

26

First of all, use a BehaviorSubject instead of EventEmitter. Change the declaration of skipCliekd to the following:

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

Then, you need to broadcast the new value using next() method as following:

this.skipClicked.next (true);

Also, change your subscription to:

 this.skipToContent.skipClicked.subscribe( value => {
     if (value === true) {
         console.log("!"); 
         // should put focus() on input 
     }
 });
FAISAL
  • 33,618
  • 10
  • 97
  • 105
  • A few problems: 1] "generatorOrNext is not a function" at SafeSubscriber.schedulerFn [as _next] (event_emitter.ts:115) 2] the event is fired automatically on pageload. i.e. first thing I get is '! '. 3] As far as I can tell, next() is deprecated in favor of emit() – DaveC426913 Aug 01 '17 at 20:17
  • EventEmmiter, that only used in a component – Ricardo D. Quiroga Apr 23 '18 at 15:02
  • 1
    EventEmitter should only be used in an actual component, Angular component integration guideline recommends using shared service as in another answer, thus downvoting because answer need update or misleading others – Regfor Oct 01 '19 at 09:47
17

EventEmitter should only be used in an actual component with an @Output directive. Anything else may not work in future.

For child-parent communication it is better to use a Subject or BehaviorSubject. This is an example from the angular guide.

https://angular.io/guide/component-interaction

@Injectable()
export class MissionService {

  // Observable string sources
  private missionAnnouncedSource = new Subject<string>();
  private missionConfirmedSource = new Subject<string>();

  // Observable string streams
  missionAnnounced$ = this.missionAnnouncedSource.asObservable();
  missionConfirmed$ = this.missionConfirmedSource.asObservable();

  // Service message commands
  announceMission(mission: string) {
    this.missionAnnouncedSource.next(mission);
  }

  confirmMission(astronaut: string) {
    this.missionConfirmedSource.next(astronaut);
  }
}

Tip:

If you have an event that just signifies something has occurred or completed - and has no actual data associated with it - you can use a Subject<void>(). This makes the signature of next() cleaner and you don't need to provide a dummy value for the sake of it.

Eg.

windowClosed = new Subject<void>();

windowClosed.next() will emit the event

Community
  • 1
  • 1
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • And yes I'm really having a hard time too when I just need the service to do ONE thing. And what do I even name it? Is it named for the component, or named for the action or named for the type of action. I guess you just have to see what works for you. – Simon_Weaver Feb 08 '18 at 23:18
  • Another key thing to remember is you can composite services that you're using for internal data transfer. They can inject into each other, and you can inject 5 services you want if you had a complex control with 5 unrelated children. I wish they had a better name than 'service' – Simon_Weaver Feb 08 '18 at 23:29