Creating an RxJS observable stream from an event emitted by a child element in an Angular 2 component template

Currently incorporating Angular 2.0.0-rc.4 alongside RxJS 5.0.0-beta.6.

In the midst of exploring various methods for generating observable streams from events, I find myself inundated with choices and would like to gather opinions. Recognizing that there is no universal solution and different techniques suit different scenarios, there may be other methods I have yet to discover or consider.

The interaction cookbook in Angular 2 offers multiple approaches for a parent component to interact with events in a child component. Among these, only the example demonstrating how parents and children communicate via a service utilizes observables, which may appear excessive for many cases.

The specific situation involves an element in the template emitting numerous events, prompting the need to periodically ascertain the most recent value.

I employ the sampleTime method of Observable set at a 1000ms interval to monitor mouse movements on an HTML <p> element.

1) This method entails using the ElementRef provided through the constructor to access the nativeElement property and query child elements by tag name.

@Component({
  selector: 'watch-child-events',
  template: `
    <p>Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];

  constructor(private el:ElementRef) {}

  ngOnInit() {
    let p = this.el.nativeElement.getElementsByTagName('p')[0];
    Observable
      .fromEvent(p, 'mousemove')
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}

This approach tends to draw criticism as Angular 2 generally provides adequate abstraction over the DOM, reducing the necessity of direct interactions. Nevertheless, the fromEvent factory method of Observable remains alluring and easily accessible, often being the first technique considered.

2) Involves utilizing an EventEmitter, essentially an Observable.

@Component({
  selector: 'watch-child-events',
  template: `
    <p (mousemove)="handle($event)">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  emitter:EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>();

  ngOnInit() {
    this.emitter
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.emitter.emit(e);
  }
}

While avoiding direct DOM querying, using event emitters primarily facilitates communication from child to parent and such events aren’t necessarily designed for output purposes.

Evidence suggests that assuming event emitters will function as observables in the final release is precarious, hinting at potential instability in relying on this feature.

3) This strategy employs an observable Subject.

@Component({
  selector: 'watch-child-events',
  template: `
    <p (mousemove)="handle($event)">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  subject = new Subject<MouseEvent>();

  ngOnInit() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}

This particular method appears well-rounded, striking a balance between effectiveness and simplicity. It affords the use of a ReplaySubject to retrieve either a history of published values upon subscription or just the latest one if available.

4) Involves using a template reference combined with the @ViewChild decorator.

@Component({
  selector: 'watch-child-events',
  template: `
    <p #p">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements AfterViewInit {
  messages:string[] = [];
  @ViewChild('p') p:ElementRef;

  ngAfterViewInit() {
    Observable
      .fromEvent(this.p.nativeElement, 'mousemove')
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}

Although functional, this method raises some concerns. Template references are mainly intended for component interactions within a template, while it also necessitates direct DOM access through nativeElement, string-based event and template reference naming conventions, and relies on the AfterViewInit lifecycle hook.

5) An extension involving a custom component managing a Subject and periodically emitting an event.

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  @Output() event = new EventEmitter<MouseEvent>();
  subject = new Subject<MouseEvent>();

  constructor() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.event.emit(e);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}

Integration within the parent component would resemble:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="handle($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent {
  messages:string[] = [];

  handle(e:MouseEvent) {
    this.messages.push(`${e.type} (${e.x}, ${e.y})`);
  }
}

This methodology presents a user-friendly approach with the custom component encapsulating the desired behavior, though communications are restricted solely within the component hierarchy without horizontal notifications.

6) Contrasted with a straightforward forwarding of events from child to parent.

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  @Output() event = new EventEmitter<MouseEvent>();

  handle(e:MouseEvent) {
    this.event.emit(e);
  }
}

Implemented in the parent using the @ViewChild decorator either as:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer>
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent implements AfterViewInit {
  messages:string[] = [];
  @ViewChild(ChildEventProducerComponent) child:ChildEventProducerComponent;

  ngAfterViewInit() {
    Observable
      .from(this.child.event)
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}

7) Or as:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="handle($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  subject = new Subject<MouseEvent>();

  ngOnInit() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}

employing an observable Subject, mirroring the previously discussed technique.

8) Lastly, in scenarios requiring broadcast notifications throughout the component tree, employing a shared service is deemed advantageous.

@Injectable()
export class LocationService {
  private source = new ReplaySubject<{x:number;y:number;}>(1);

  stream:Observable<{x:number;y:number;}> = this.source
    .asObservable()
    .sampleTime(1000);

  moveTo(location:{x:number;y:number;}) {
    this.source.next(location);
  }
}

This design encapsulates the functionality within the service. The child component simply needs to inject the LocationService in the constructor and invoke moveTo within the event handler.

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  constructor(private svc:LocationService) {}

  handle(e:MouseEvent) {
    this.svc.moveTo({x: e.x, y: e.y});
  }
}

Inject the service at the required level within the component hierarchy for broadcasting purposes.

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer>
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent],
  providers: [LocationService]
})
export class WatchChildEventsComponent implements OnInit, OnDestroy {
  messages:string[] = [];
  subscription:Subscription;

  constructor(private svc:LocationService) {}

  ngOnInit() {
    this.subscription = this.svc.stream
      .subscribe((e:{x:number;y:number;}) => {
        this.messages.push(`(${e.x}, ${e.y})`);
      });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Remember to unsubscribe when done. While offering flexibility, this solution introduces added complexity.

To summarize, I opt for an internal Subject within the component if inter-component communication isn't necessary (3). When communication up the component tree is required, I prefer encapsulating a Subject in the child component and applying stream operators within the component itself (5). For maximum flexibility across the entire component tree, utilizing a service to wrap a stream proves most effective (8).

Answer №1

Method 6 introduces a more streamlined approach by utilizing event binding (refer to "Custom Events with EventEmitter") in place of using @ViewChild and ngAfterViewInit:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="onEvent($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent {
  messages:string[] = [];
  onEvent(e) { this.messages.push(`${e.type} (${e.x}, ${e.y})`); }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Guide to generating a dropdown menu and linking it with data received from a server

I am completely new to Angular and recently received a project involving it. My task is to create a nested dropdown list for Json data retrieved from a server using Rest Api calls. The data contains a Level attribute that indicates the hierarchy level of ...

Efficiently integrating Firebase Functions with external sub path imports beyond the project's

I encountered an issue in my firebase functions project with typescript. The problem arises when I use types from outside the project with sub path imports, causing the build files to become distorted. Instead of having main:lib/index.js, I have main:lib/ ...

Having trouble executing ng build --prod in Azure CICD pipelines

Having trouble setting up my application's CI/CD in Azure as the build process keeps failing. I've gone through my YAML configuration and tried multiple solutions found online, but it still doesn't work. This is the YAML setup I have: ...

Executing the Ionic code within the Xcode Swift environment

I have developed an Ionic application that I successfully compiled in Xcode for iOS. Additionally, I have integrated a widget into the application. My goal is to set it up so that when the application is opened using the regular app icon, it displays the m ...

Adjusting the vertical dimension of an Angular 17 Material Dropdown Field?

Check out this demo where I'm exploring ways to decrease the height of the select field. Here's the code snippet: <mat-form-field appearance="outline"> <mat-label>Toppings</mat-label> <mat-select [formControl]=& ...

Updating state in React without providing a key prop is a common issue, especially when

Currently, I am working on implementing a Radio Group where I want the radio button's checked value to update when another button is clicked. In the example provided below, it seems that the desired effect can only be achieved using the "key" prop. Is ...

What is the most recent stable version of Angular recommended for utilizing ui-bootstrap?

I've been working on updating an older angular application and I'm interested in incorporating ui bootstrap for more advanced features. However, the current version of Angular used is 1.2.18 and any attempt to upgrade it to a higher version resul ...

Creating a custom `onSubmit` function with Formik, TypeScript, and hooks can be a powerful way

I'm currently creating form onSubmit functions utilizing the useCallback hooks specifically designed for use with the formik library. A sample structure of my component using formik would be as follows: import { useContactForm } from './useCon ...

Encountering a Problem Configuring Jest in Angular 9 Using angular-jest-preset

Currently in the process of setting up Jest for Angular 9 using jest-preset-angular version 9. The code is running, but encountering the error: Error: Cannot read property 'ngModule' of null Uncertain on how to troubleshoot this issue. https:/ ...

Utilize an array of observables with the zip and read function

I'm struggling with putting an array of observables into Observable.zip. I need to create a function that reads values from this dynamically sized array, but I'm not sure how to go about it. Does anyone have any suggestions? import {Observable} ...

Why are @Inject and Injectable important in Angular's Dependency Injection system?

constructor(private smartphoneService: smartphoneService) { } Although I am able to execute the code above without encountering any errors, I find myself pondering on the necessity of using @Inject and Injectable on services, Pipes, and other components. ...

Tips for syncing the state data stored in local storage across all tabs with Ngxs state management

After converting the state data to base64 format using the Ngxs state management library, I am saving it. While I can retrieve all data across different tabs, any changes made in one tab do not automatically sync with other tabs. A tab refresh is required ...

Managing enum types with json2typescript

After receiving a JSON response from the back-end that includes an Enum type, I need to deserialize it. The JSON looks like this: { ..., pst:['SMS','EMAIL'], ... } In Typescript, I have defined my enum class as follows: export enum Pos ...

Implementing asynchronous data sharing within an Angular 2 service

I seem to be facing a challenge that I can't quite figure out. My goal is to share data asynchronously between components that I receive from a server. Here is an example of what my service code looks like: import {Injectable} from 'angular2/co ...

What is the process for retrieving the chosen country code using material-ui-phone-number?

When incorporating user input for phone numbers, I have opted to utilize a package titled material-ui-phone-number. However, the challenge arises when attempting to retrieve the country code to verify if the user has included a 0 after the code. This infor ...

What is the solution to prevent angular-material components from covering my fixed navbar?

After creating a navbar using regular CSS3, I incorporated input fields and buttons from angular material. However, my sticky navbar is being obscured by the components created with angular material. Here is the CSS for the navbar: position: sticky; top: ...

The firebase.d.ts on iOS functions properly, whereas on Android, it becomes

Currently, I am working with Ionic 2 on my Mac system. Your system information: Cordova CLI: 6.4.0 Ionic Framework Version: 2.0.0-rc.4 Ionic CLI Version: 2.1.18 Ionic App Lib Version: 2.1.9 Ionic App Scripts Version: 1.0.0 ios-deploy version: Not instal ...

What is the best approach to implementing role-based authentication within a MEAN application?

Currently, I am developing a mean stack application and looking to implement role-based authentication. For instance, if the user is an admin, they should have additional permissions and access rights. Any guidance on implementing this feature would be g ...

Updating the node startup file with Visual Studio 2015 using NodeJS/Typescript

Encountering a persistent error: Error Code: TS5055 Cannot write file C:/project/dir/server.js' because it would overwrite the input file. Project: TypeScript/JavaScript Virtual Projects Even after renaming my entry filename to nodeserver.js, the ...

RxJS failing to properly merge the outcome of a subsequent HTTP request

Currently, I have a searchbox that utilizes three separate search APIs. Users can input either a string or a number. The API returns results differently based on the input type: for strings, it may return up to ten results; for numbers, a second GET reques ...