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

The Angular 2 application functions perfectly when running locally, but encounters issues when running on an ec2 instance

When trying to upload an Angular 2 application to an AWS EC2 t2.small instance, it is not working as expected, even though it runs successfully in a local server. Node version: v7.0.0 NPM version: 3.10.8 There has been an EXCEPTION: Uncaught (in prom ...

Having trouble fixing TypeScript bugs in Visual Studio Code

I am encountering a similar issue as discussed in this solution: Unable to debug Typescript in VSCode Regrettably, the suggested solution does not seem to resolve my problem. Any assistance would be greatly appreciated. My directory structure looks like ...

Tips for receiving string body parameters from Express routes in TypeScript instead of using the 'any' type?

I have a situation where I am passing a unique identifier called productId as a hidden input within a form: <form action="/cart" method="POST"> <button class="btn" type="submit">Add to Cart</button ...

What is the process for creating a jQuery object in TypeScript for the `window` and `document` objects?

Is there a way to generate a jQuery object in TypeScript for the window and document? https://i.stack.imgur.com/fuItr.png ...

To reveal more options, simply click on the button to open up a dropdown

I am currently utilizing the Bootstrap 5 CDN and I am looking for a way to automatically open the dropdown menu without having to physically click on it. Within the navbar, I want to display the "Dropdown Link": <nav class="navbar navbar-expand-sm ...

Attempting to utilize pdf.js may result in an error specifying that pdf.getPage is not a recognized function

After installing pdfjs-dist, I attempted to extract all text from a specific PDF file using Node and pdfjs. Here is the code I used: import pdfjs from 'pdfjs-dist/build/pdf.js'; import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry.js&a ...

What is the syntax for accessing elements from an iterable?

Is it possible to create a getter that acts like a function generator? My attempts class Foo { * Test1(): IterableIterator<string> { // Works, but not a getter... yield "Hello!"; } * get Test2(): IterableIterator<string> ...

Breaking down an object using symbols as keys in Typescript

I'm encountering an error when running this code Type 'symbol' cannot be used to index type '{ [x: string]: string; }'.: let symbol = Symbol() let obj = { [symbol] : 'value'} let { [symbol]: alias } = obj // ...

Enhance your coding experience with code completion and autocomplete in Angular/Typescript using ATOM within

Is it possible to have Codecompletion / Autocomplete in Atom similar to Webstorm? Currently I am getting familiar with TypeScript and really enjoying it, but the lack of Codecompletion support for my HTML files in Atom is quite frustrating. Having this f ...

Creating a TypeScript NPM package that provides JSX property suggestions and autocomplete functionality for Intellisense

I developed a compact UI toolkit and released it on the NPM registry. The library is built using strongly typed styled-components (TypeScript) and can be easily integrated into React applications. It functions perfectly after installation with npm install ...

Tips for securely passing props based on conditions to a functional component in React

I came across this situation: const enum Tag { Friday: 'Friday', Planning: 'Planning' } type Props = { tag: Tag, // tour: (location: string) => void, // time: (date: Date) => void, } const Child: React.FC<Props> = ...

Trouble with Firebase Setup in Ionic 4+ Web Application

I'm currently trying to establish a connection between my ionic application and Firebase for data storage, retrieval, and authentication. Despite using the npm package with npm install firebase, I encountered an error message that reads: > [email& ...

Failure in Dependency Injection in Angular with Typescript

My mobile application utilizes AngularJS for its structure and functionality. Below is the code snippet: /// <reference path="../Scripts/angular.d.ts" /> /// <reference path="testCtrl.ts" /> /// <reference path="testSvc.ts" /> angular.mo ...

Rendering illuminated component with continuous asynchronous updates

My task involves displaying a list of items using lit components. Each item in the list consists of a known name and an asynchronously fetched value. Situation Overview: A generic component named simple-list is required to render any pairs of name and va ...

Is there a way to customize a chart in Ionic 2 to resemble the image provided?

Hello there, I am currently using import {Chart} from 'chart.js'; to generate my chart; however, I am facing some difficulties. My goal is to create a chart similar to the one displayed below. Warm regards //Generating the doughnut this.dou ...

TypeScript enables the use of optional arguments through method overloading

Within my class, I have defined a method like so: lock(key: string, opts: any, cb?: LMClientLockCallBack): void; When a user calls it with all arguments: lock('foo', null, (err,val) => { }); The typings are correct. However, if they skip ...

Using TypeScript to Add Items to a Sorted Set in Redis

When attempting to insert a value into a sorted set in Redis using TypeScript with code like client.ZADD('test', 10, 'test'), an error is thrown Error: Argument of type '["test", 10, "test"]' is not assigna ...

Enable the parsing of special characters in Angular from a URL

Here is a URL with special characters: http://localhost:4200/auth/verify-checking/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="59663c34383035643230383d2b606a6e6b686d6e6e193e34383035773a3634">[email protected]</a> ...

...additional properties in React function components using TypeScript

Here is a snippet of code that I am working with: <InputComponent id="email" name={formik.values.email} type="text" formik={formik} className="signInInput" disabled/> However, there seems to be an issue with the disable ...

What are the steps to setting up SystemJS with Auth0?

I am having trouble configuring SystemJS for Auth0 (angular2-jwt) and Angular 2.0.0-beta.6 as I keep encountering the following error message: GET http://localhost:3000/angular2/http 404 (Not Found)fetchTextFromURL @ system.src.js:1068(anonymous function) ...