What is the best way to implement event handling for multi-level components/templates in Angular 5?

Currently, I am immersed in a project using Angular 5. One of the templates, called Template A, is filled with various HTML elements. I am incorporating Template A into another template, Template B, which offers additional functionalities on top of Template A. Template B includes buttons and other features.

Template A

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

Template B

 <html>
   <templateA></templateA>
   <button (click)="onSubmit()")
 </html>

At the moment, all my components (from Component A to Z) reference Template B as their templateURL for reusability. Each component can have its own onSubmit() event triggered from Template B, with each component having its unique definition. This system functions well.

However, I am interested in incorporating an (onChange) event in one of the elements within Template A, with each component being able to define their own event listener (Component A-Z).

Answer №1

To create a custom service called a.service.ts, follow the code below:

Service

@Injectable()
export class AService {
  private subject = new Subject<any>();
  constructor() { }
    sendEvent(eventName: string) {
        this.subject.next({ event: eventName });
    }

    getEvent(): Observable<any> {
        return this.subject.asObservable();
    }
}

This service can be utilized in any Component (A-Z). To subscribe and trigger events in a component, implement the following in the component:

Component

eventName: any;
subscription: Subscription;

constructor(private aService: AService) {
    this.subscription = this.aService.getEvent()
          .subscribe(event=> { 
               this.eventName = event;  // Perform necessary actions
               if(this.eventName == 'elemAClicked') {
                  // Your desired functionality here
               }
          });
}

ngOnDestroy() {
    // Unsubscribe to avoid memory leaks
    this.subscription.unsubscribe();
}
// Method that can be used in the template, for example onClickAElem
onClickAElem() {
   this.aService.sendEvent('elemAClicked');
}

Answer №2

As pointed out by @bryan60, this question pertains more to architecture rather than a specific application state issue.

In my view, a generic approach is needed to efficiently share data across multiple components using events.

This will enable a flexible implementation that can be easily utilized throughout the application.

In my Angular app, I have employed an event service based on observables, allowing components to trigger events that other components can listen to -

Event Service

import { Injectable } from "@angular/core";
import * as Rx from 'rxjs/Rx';
import { Observable, Subject } from "rxjs/Rx";
import { UUID } from "./uuid-generator";

export const GLOBAL_EVENTS = {
    Test_Event: "Test_Event";
}

/**
 * 
 * 
 * @class EventListener
 */
class EventListener {

    constructor(uuid: string, func: Function) {
        if (!uuid || !func) {
            console.error("required arguments not provided!");
            return;
        }
        this._uuid = uuid;
        this.func = func;
    }

    private _uuid: string;
    public get uuid(): string {
        return this._uuid;
    }

    public func: Function;
}

@Injectable()
export class EventsService {

    private eventsSubject: Subject<any>;
    private listeners: Map<string, Array<EventListener>>;

    public constructor() {
        //initialise listeners
        this.listeners = new Map();
        //initialise event subject
        this.eventsSubject = new Rx.Subject();
        //listen to the changes in subject
        Rx.Observable.from(this.eventsSubject)
            .subscribe(({ name, args }) => {
                if (this.listeners[name]) {
                    this.listeners[name].forEach((listener: EventListener) => {
                        listener.func(...args);
                    });
                }
            });
    }

    /**
     * Listens to the event and provides a unique key for the listener
     * @method on
     * @param {string} name name of the event to be listened to.
     * @param {Function} listener listener function with one array argument
     * @return {string} returns the key (UUID) for listener function which can be used to stop listening to the event.
     * @memberof EventsService
    */
    public on(name: string, listener: Function): string {
        this.listeners[name] = this.listeners[name] || [];
        let listenerEvent: EventListener = new EventListener(UUID.generate(), listener);
        this.listeners[name].push(listenerEvent);
        return listenerEvent.uuid;
    }

    /**
     * Stops listening to the event by using the unique key for the listener
     * @method off
     * @param {string} name name of the event to be broadcasted
     * @param {string} uuid name of the event listener specific uuid to be removed
     * @param {boolean} [removeAll=false] removes all event listeners attached to specified event name
     * @returns {void} 
     * @memberof EventsService
    */
    public off(name: string, uuid?: string, removeAll: boolean = false): void {
        if (!this.listeners[name] || !this.listeners[name].length) {
            return;
        }
        if (removeAll) {
            this.listeners[name] = [];
            return;
        }
        this.listeners[name] = this.listeners[name].filter((item: EventListener, index: number) => {
            return item.uuid !== uuid;
        });
    }

    /**
     * Broadcasts the event and passes data provided in args argument as event data
     * @method broadcast
     * @param {string} name name of the event to be broadcasted.
     * @param {any} args arguments to be sent with the event.
     * @return {void} return void
     * @memberof EventsService
    */
    public broadcast(name: string, ...args): void {
        this.eventsSubject.next({ name, args });
    }
}

UUID Generator

export class UUID {
    public static generate(): string {
        return "your_hash";
    }
}

Registering Event

import { Component, OnDestroy } from "@angular/core";
import { EventsService, GLOBAL_EVENTS } from "./events.service";

@Component({
    selector: "[app-listener-component]",
    templateUrl: "./app-listener-component.html",
    styleUrls: ["./app-listener-component.scss"]
})
export class ListenerComponent {

    private eventId: string;

    public constructor(private _EventsService: EventsService) {
        this.eventId = this._EventsService.on(GLOBAL_EVENTS.Test_Event, (response: any) => {
            console.log(response);            
        });
    }

    public ngOnDestroy(): void {
        this._EventsService.off(this.eventId);
    }
}

Broadcasting Message

import { EventsService, GLOBAL_EVENTS } from "./events.service";

export class BroadcatComponent
    public constructor(private _EventsService: EventsService) {
        let data: any = {
            message: "any"
        }
        this._EventsService.broadcast(GLOBAL_EVENTS.Test_Event, data);
    }
}

Things to Remember

Ensure to include the service in the main app module for it to function as a singleton service across the application.

Each event subscription provides a unique UUID string that can be utilized to remove the event handler globally (

additional features are in code comments
).

Utilizing ngDestroy to eliminate event handlers ensures that unused events do not linger in memory.

Lastly, I have made modifications to the original code, so compilation errors may arise in the sample code.

I trust this information proves useful :)

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

Is it possible to preserve the numerical type of a percentage when applying number formatting?

After applying number formatting, I converted a numerical value of 150 to 150.00%. Although this is the desired display format with the percentage sign included, the data type remains as string instead of number. Is there a method to convert it back to a ...

Visualization of pie charts in Angular2 using Google library

Currently, I am in the process of building a web application using Angular2 that includes a Google pie chart. One of the components of the application is a Directive specifically designed for the pie chart. Below is the code snippet for the Directive: @D ...

The angular datepicker functionality seems to be malfunctioning

unique-example encountering this error message: error TS2307: Cannot find module 'mydatepicker' also encountering a compile time error at this line: import { MyDatePickerModule } from 'mydatepicker'; report.module.ts : import ...

Cropping and resizing images

Within my angular application, I have successfully implemented image upload and preview functionality using the following code: HTML: <input type='file' (change)="readUrl($event)"> <img [src]="url"> TS: readUrl(event:any) { if ...

The parameters 'event' and 'payload' do not match in type

Upon running the build command, I encountered a type error that states: Type error: Type '(event: React.FormEvent) => void' is not assignable to type 'FormSubmitHandler'. Types of parameters 'event' and 'payload&apos ...

Display JSON data in Angular view by extracting the desired value

Hey there! I have a GET response data with a field like "DeletionDate": "0001-01-01T00:00:00". What I'm trying to achieve is to remove the time part T00:00:00 and only display half of the value in my views. Is there a way to trim the value and show it ...

Switching from HttpModule to HttpClientModule

Angular's transition from HttpModule to HttpClientModule is causing some confusion, as discussed in detail here. The official Angular tutorial at https://angular.io/tutorial/toh-pt6 still uses HttpModule, while the Fundamentals documentation at https ...

Sending JSON information from the App Component to a different component within Angular 6

I am faced with a challenge involving two components: 1. App Component 2. Main Component Within app.component.ts: ngOnInit () { this.httpService.get('./assets/batch_json_data.json').subscribe(data => { this.batchJson = data ...

A method for consolidating multiple enum declarations in a single TypeScript file and exporting them under a single statement to avoid direct exposure of individual enums

I am looking to consolidate multiple enums in a single file and export them under one export statement. Then, when I import this unified file in another file, I should be able to access any specific enum as needed. My current setup involves having 2 separ ...

LocalStorage in Angular application failing to function in Android web view

Working with Angular 4, I've developed a web application that stores the user security token in localStorage after login. The application uses this localStorage value to control the visibility of certain links, such as hiding the login button once th ...

Talebook: Unable to modify UI theming color

As I embark on creating my own theme in Storybook, I am closely following the guidelines outlined here: Currently, I have copied the necessary files from the website and everything seems to be working fine. However, I am facing an issue when trying to cus ...

The angulartics2 event is not functioning correctly

I have implemented the angulartics2 library in my project. import { Component } from '@angular/core'; import { Angulartics2On } from 'angulartics2'; @Component({ selector: 'random-component', directives: [Angulartics2On] ...

Is there a way to show text as symbols in Primeng components?

Check out this helpful example that demonstrates what I am attempting to achieve. I have implemented p-menu from primeng. Is there a method to convert text into symbols like &trade to ™ and &#8482 to ®? ...

Methods for bypassing a constructor in programming

I am working on a code where I need to define a class called programmer that inherits from the employee class. The employee class constructor should have 4 parameters, and the programmer class constructor needs to have 5 parameters - 4 from the employee c ...

When utilizing Typescript with React Reduxjs toolkit, there seems to be an issue with reading the state in useSelector. An error message is displayed indicating that the variable loggedIn

I have encountered an error while passing a state from the store.tsx file to the component. The issue lies in the const loggedIn where state.loggedIn.loggedIn is not recognized as a boolean value. Below is the code snippet for the component: import React ...

Is it possible to generate a property for an interface by casting a key within a for-in loop?

When I attempt to set a property on an object with a value from a dynamically generated form, I utilize a for-in loop to identify a property in the object and assign it. FormFeatureArray.forEach((el) => { // form handling stuff omitted For(c ...

Unsubscribing from a nested observable - a step-by-step

In our Angular component, we leverage the ngOnDestroy() method to handle canceling http requests that are still pending when navigating away from a page. To avoid reloading data that has already been fetched, we utilize a custom generic cache helper on cer ...

Exploring the Angular Heroes Journey: What's the significance of one being labeled with a colon while the other is identified

Setting: Angular 5+ Source: https://angular.io/tutorial Within the heroes.component.ts class, we see an assignment using a colon: export class HeroesComponent implements OnInit { heroes: Hero[]; However, in the app.component.ts class, a different as ...

Checkbox selection can alternate based on conditions for formgroup controls

My FormGroup named 'fruits' has been set up fruits: FormGroup = formBuilder.group({ numberOfFruits: [0,Validators.min(0)], apple: false, mangoes: false }); Here is the corresponding HTML code: <div formGroupName ...

Executing functions before the data is loaded in an Angular application

Hey there, I'm relatively new to Angular and I've been facing some difficulties when it comes to loading data. In the code snippet below, you'll notice that I have two services being called. Each service logs something to the console. After ...