Angular click throttle

Within my template, there is a field along with two buttons:

<div class="btn-plus" (click)="add(1)"> - </div>
<div class="txt"> {{ myValue }} </div>
<div class="btn-minus" (click)="add(-1)"> + </div>

In the related .ts file of the component, the following code exists:

add(num) {
    this.myValue += num;
    this.update(); // An asynchronous function that triggers a PUT request
}

The this.update() function extracts myValue and includes it in a specific field within a large JSON object before sending it to the server.

Issue: When a user rapidly clicks the plus/minus button multiple times, the request is being sent every time. The goal is to only send one request, half a second after the last click. How can this be achieved?

Answer №1

Implement the takeUntil operator like this:

export class AppComponent  {
  name = 'Angular';

  calls = new Subject();

  service = {
    getData: () => of({ id: 1 }).pipe(delay(500)),
  };

  click() {
    this.calls.next(true);
    this.service.getData().pipe(
      takeUntil(this.calls),
    ).subscribe(res => console.log(res));
  }
}

Visit Stackblitz (open your browser's console to view the logs)

Answer №2

This answer was discovered partially on the internet, but I am open to exploring alternative solutions or enhancements to the existing solution (directive):

During my online search, I came across a appDebounceClick directive that aided me in the following manner:

I eliminated update and replaced it with add in the .ts file:

add(num) {
    this.myValue += num;
}

I then modified the template as follows:

<div 
    appDebounceClick 
    (debounceClick)="update()" 
    (click)="add(1)" 
    class="btn-plus"
    > - 
</div>
<div class="txt"> {{ myValue }} </div>
<!-- similar for btn-minus -->

BONUS

The appDebounceClick directive was created by Cory Rylan. Here is the code snippet (in case the link becomes inaccessible in the future):

import { Directive, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import { debounceTime } from 'rxjs/operators';

@Directive({
  selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit, OnDestroy {
  @Input() debounceTime = 500;
  @Output() debounceClick = new EventEmitter();
  private clicks = new Subject();
  private subscription: Subscription;

  constructor() { }

  ngOnInit() {
    this.subscription = this.clicks.pipe(
      debounceTime(this.debounceTime)
    ).subscribe(e => this.debounceClick.emit(e));
  }

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

  @HostListener('click', ['$event'])
  clickEvent(event) {
    event.preventDefault();
    event.stopPropagation();
    this.clicks.next(event);
  }
}

Answer №3

After experimenting with various solutions, I ultimately opted for a streamlined version of the DebounceClickDirective provided earlier. Since the debounceTime operator lacked support for leading/trailing options, I turned to utilizing the power of lodash. This adjustment effectively removed the delay between clicking and triggering an action, particularly useful in scenarios where swift responsiveness is crucial.

The implementation was straightforward, simply incorporating it as follows:

<button (debounceClick)="openDialog()">

import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
import { debounce } from 'lodash';

@Directive({
  selector: 'button',
})
export class DebounceClickDirective {
  @Output() debounceClick = new EventEmitter();

  @HostListener('click', ['$event'])
  debouncedClick = debounce(
    (event: Event) => {
      this.debounceClick.emit(event);
    },
    500,
    { leading: true, trailing: false },
  );
}

Answer №4

A utility function that helps improve performance --

export const debounced = (callback, delay) => {
  const db = new Subject();
  const sub = db.pipe(debounceTime(delay)).subscribe(callback);
  const func = value => db.next(value);

  func.unsubscribe = () => sub.unsubscribe();

  return func;
};

For instance, it can be implemented like this:

import { Component, OnInit } from '@angular/core';
import { debounced } from 'src/helpers';

@Component({
  selector: 'app-example',
  // Click triggers `debouncedClick` instead of `myClick` directly
  template: '<button (click)="debouncedClick($event)">Click This</button>'
})
export class Example implements OnDestroy {
  debouncedClick; // Subject.next function

  constructor() {
    // Assigning in the constructor or ngOnInit for proper resolution of `this`
    this.debouncedClick = debounced($event => this.myClick($event), 800);
  }

  // Executes after the debounce delay (800ms from last call)
  myClick($event) {
    console.log($event);
  }

  ngOnDestroy() {
    // Keep things clean!
    this.debouncedFunc.unsubscribe();
  }
}

You can also reverse the usage by calling 'myClick' on click and letting the debounced callback handle the desired action. It's up to personal preference.

I personally find this useful for handling (keyup) events too.

Not entirely certain if the unsubscribe is necessary - opted for it as a quick fix rather than delving into memory leak investigation :)

Answer №5

If you prefer not to use an rxjs observable, you can achieve the same functionality using a setTimeout. Below is a sample implementation that ensures memory leak cleanup on ngOnDestroy:

@Component({
  selector: "app-my",
  templateUrl: "./my.component.html",
  styleUrls: ["./my.component.sass"],
})
export class MyComponent implements OnDestroy {
  timeoutRef: ReturnType<typeof setTimeout>;

  clickCallback() {
    clearTimeout(this.timeoutRef);
    this.timeoutRef  = setTimeout(()=> {
      console.log('finally clicked!')
    }, 500);
  }

  ngOnDestroy(): void {
    clearTimeout(this.timeoutRef);
  }
} 

Edit: timeoutRef TypeScript definition has been updated based on feedback from @lord-midi in the comments section.

Answer №6

Is it possible to trigger the onClick event only once, without waiting for subsequent clicks? Or is there a way to ignore multiple click events?

Check out the solution provided by Ondrej Polesny on FreeCodeCamp's website. Special thanks to Cory Rylan for his detailed explanation of Debouncer.

import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';

@Directive({
  selector: '[appDebounceClick]'
})

export class DebounceClickDirective {

  @Input() debounceTime: number = 800;
  @Output() debounceClick: EventEmitter<any> = new EventEmitter();

  private onClickDebounce = this.debounce_leading(
    (e) => this.debounceClick.emit(e), this.debounceTime
  );

  @HostListener('click', ['$event'])
  clickEvent(event) {
    event.preventDefault();
    event.stopPropagation();
    this.onClickDebounce(event);
  }

  private debounce_leading(func: any, timeout: number) {
    let timer;
    return (...args) => {
      if (!timer) {
        func.apply(this, args);
      }
      clearTimeout(timer);
      timer = setTimeout(() => {
        timer = undefined;
      }, timeout);
    };
  };

}

Answer №7

An easier way to approach this is by utilizing a custom subject that triggers on click events and leveraging the built-in functionality of RxJS with debounceTime. See it in action (check the console logs): Stackblitz

// set up these variables
clicker = new Subject();
clicker$ = this.clicker.asObservable();

//Include this inside ngOnInit(), adjust the debounce time as needed
this.clicker$.pipe(debounceTime(200)).subscribe(() => {
     console.log('Requesting Data ...');
     this.service.getData().subscribe((d) => console.log(d));
});

// Call this in your button's click function:
this.clicker.next(true);

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

Using Angular 4 to retrieve a dynamic array from Firebase

I have a dilemma while creating reviews for the products in my shop. I am facing an issue with the button and click event that is supposed to save the review on the database. Later, when I try to read those reviews and calculate the rating for the product, ...

What are the steps to integrating a repository into the clean architecture design pattern?

I have been following Uncle Bob's clean architecture principles in developing my medical application's API. However, I am facing some challenges in determining where certain components should be implemented. Within my application layer, I have a ...

Having difficulty retrieving JSON properties that include hyphens or numerical characters in the key - a challenge with Rollup and TypeScript

Trying to load a large JSON file using import. This JSON file contains an object with keys that have numbers and hyphens. The issue arises when trying to access keys with hyphens or numbers as it returns undefined. Using Rollup for building due to Svelte ...

Utilizing TypeScript with React to dynamically select which component to render

My task seemed simple at first: to render a React component based on its name/type. Here is an example of how it is used: // WidgetsContainer.ts // components have a difference in props shape! const componentsData = [ { type: 'WIDGET_1', ...

Using Typescript to dynamically set the length of a tuple or array based on the arguments passed to a function

Here is some TypeScript code to consider: function concatTuples<T>(first: [...T[]], second: [...T[]]): [...T[]] { return [...first, ...second]; } const result = concatTuples([0, 1], [2, 3]); This function concatenates tuples. The current challeng ...

Running Jasmine asynchronously in a SystemJS and TypeScript setup

I am currently executing Jasmine tests within a SystemJS and Typescript environment (essentially a plunk setup that is designed to be an Angular 2 testing platform). Jasmine is being deliberately utilized as a global library, rather than being imported vi ...

Dynamically using setTimeout and clearTimeout in multiple Angular2 instances

let timerIdentifier; deleteTimer(id){ timerIdentifier = id; timerIdentifier = setTimeout(()=>{ //some logic },5000); } undoTimer(id){ timerIdentifier = id; clearTimeout(timerIdentifier); } I need help to implement a logic in Angular2 wh ...

Is there a way to use Regex to strip the Authorization header from the logging output

After a recent discovery, I have come to realize that we are inadvertently logging the Authorization headers in our production log drain. Here is an example of what the output looks like: {"response":{"status":"rejected",&quo ...

"Securing your Angular application: A guide to encrypting and decrypting

Within my current project, there are two main modules: Staff and Agent. When I click on the Agent module list, the URL displays as "Agent/list" and when updating an Agent, the corresponding ID is passed in the URL. However, I am interested in passing enc ...

Issue with Ag grid rendering in Angular 2 with webpack 2 configuration not displaying properly

I'm currently attempting to integrate ag-grid into my Angular2 project, but I'm experiencing difficulties with rendering. I'm using the ag-grid package and following a tutorial for a .NET project generated with the command 'dotnet new ...

Initialization of an empty array in Typescript

My promises array is structured as follows: export type PromisesArray = [ Promise<IApplicant> | null, Promise<ICampaign | ICampaignLight> | null, Promise<IApplication[]> | null, Promise<IComment[]> | null, Promise<{ st ...

Is there a way to establish a connection between two excel entries using Angular?

In order to connect xlsx file records with their corresponding ids using angular, I am seeking a solution. To elaborate further: Let me provide an example for better understanding: Scenario 1 https://i.stack.imgur.com/25Uns.png Scenario 2 https://i ...

What is the process for refreshing the dropdown menu in angular2 after modifying the data in the typescript file?

Right now, I am implementing angular2 in my project. I have a dropdown component labeled as CFC Filter {{val}} In the typescript file, I have defined this.filters = ["0", "60", "100", "180", "600", "1000"]; If the filter value retrieved from the da ...

What is the best way to generate an object in TypeScript with a variety of fields as well as specific fields and methods?

In JavaScript, I can achieve this using the following code: var obj = { get(k) { return this[k] || ''; }, set(k, v) { this[k] = v; return this; } }; obj.set('a', 'A'); obj.get('a'); // returns &ap ...

Utilizing Socket.io alongside Express.js routes for real-time communication

I have been working with socketio and expressjs, using routes as middleware. However, my current setup is not functioning as intended. I have provided the code snippets below, but none of them seem to be working. Can someone please help me troubleshoot thi ...

The loginError variable in the ts file may experience a delay in updating its value

After entering an incorrect email and password, clicking on submit should set this.LoginError to return true in the component ts file console. However, it initially returns false, and only after clicking submit two or three times does the value finally upd ...

Angular 5 in conjunction with Keycloak enabling access for non-authenticated users

I have a situation where I need to implement anonymous user access in my app, and for that purpose, I am incorporating the 'keycloak-angular' library along with Angular 5. The documentation instructs me to initialize the application as shown belo ...

Develop a child component that features a generic form control

Hello, I am looking to develop a child component that will display content for each form control within a large form group. However, simply passing the form control itself is not possible because it requires a form group. My approach involves injecting t ...

Utilizing Angular and TypeScript: The best approach for managing this situation

I need some guidance on handling asynchronous calls in Angular. Currently, I am invoking two methods from a service in a controller to fetch an object called "categoryInfo." How can I ensure that these methods return the categoryInfo correctly and displa ...

Whenever the route changes in Angular, the components are duplicated

Whenever I switch routes in my Angular application, such as going from home to settings and back to home, all the variables seem to be duplicated from the home page and are never destroyed. I noticed that I created a loop in the home component that displa ...