1

I have a problem with RxJs.

I need to log a message in console.log when I click once, and different message when I click twice on the button. The problems are:

  • at the beginning if I click first time - nothing happens - wrong (I should see first message 'one click')

  • then if I click on the button and then (after one second) I click again, for the second click I can see two messages - wrong (I should see only one message per action)

  • when I click on the button, wait a second, click again, wait a second and click again, then for the last click I will see three messages in console.log - wrong (I should see only one message per action)
  • after that if I click twice (doubleclick) I will see five messages for double click, and one for one click - wrong (I should see only one message 'double click')

all I want is:

  • if I click once, I need to see only one message ('one click')
  • if I click twice (doubleclick), still I need to see only one message ('double click')
  • if I click more than twice - nothing happens

any help?

check hear for examples

Dulanga Heshan
  • 1,335
  • 1
  • 19
  • 36
Przemo
  • 477
  • 1
  • 7
  • 16
  • 2
    please put your code in your question as well – bryan60 Sep 05 '18 at 20:47
  • yeah I forget, thx – Przemo Sep 05 '18 at 20:48
  • have you tried angulars (dblclick) binding instead of a complex observable? – bryan60 Sep 05 '18 at 20:50
  • of course, but I need another solution for ios where dblclick is not working properly – Przemo Sep 05 '18 at 20:52
  • 1
    FYI, the reason you're getting multiples of each message is because you are resubscribing to the observable every time the `test()` function is called. You should unsubscribe after the observable subscription callback fires. Which begs the question, why are you using observables in the first place? Seems like a regular click handler should do, yeah? – mhodges Sep 05 '18 at 21:24

3 Answers3

4

A very simple answer (not using any observables) is to use a setTimeout() and check on each click if the timeout is already set, if so, you know it's a second click within a given time window (double click) and if not, it's the first click. If the timeout expires, you know it was just a single click, like so:

Updated StackBlitz

// have a timer that will persist between function calls
private clickTimeout = null;
public test(event): void {
  // if timeout exists, we know it's a second click within the timeout duration
  // AKA double click
  if (this.clickTimeout) {
    // first, clear the timeout to stop it from completing
    clearTimeout(this.clickTimeout);
    // set to null to reset
    this.clickTimeout = null;
    // do whatever you want on double click
    console.log("double!");
  } else {
  // if timeout doesn't exist, we know it's first click
    this.clickTimeout = setTimeout(() => {
      // if timeout expires, we know second click was not handled within time window
      // so we can treat it as a single click
      // first, reset the timeout
      this.clickTimeout = null;
      // do whatever you want on single click
      console.log("one click");
    }, 400);
  }
}

EDIT

I missed the part about ignoring any more than 2 clicks. It's not that much more work, but I broke it out a bit more to be able to reuse code, so it looks like a lot more. Anyway, to ignore 3+ clicks, it would look like the following:

// count the clicks
private clicks = 0;
private clickTimeout = null;
public test(event): void {
  this.clicks++;
  if (this.clickTimeout) {
    // if is double click, set the timeout to handle double click
    if (this.clicks <= 2) {
      this.setClickTimeout(this.handleDoubleClick);
    } else {
    // otherwise, we are at 3+ clicks, use an empty callback to essentially do a "no op" when completed
      this.setClickTimeout(() => {});
    }
  } else {
    // if timeout doesn't exist, we know it's first click - treat as single click until further notice
    this.setClickTimeout(this.handleSingleClick);
  }
}
// sets the click timeout and takes a callback for what operations you want to complete when the
// click timeout completes
public setClickTimeout(cb) {
  // clear any existing timeout
  clearTimeout(this.clickTimeout);
  this.clickTimeout = setTimeout(() => {
    this.clickTimeout = null;
    this.clicks = 0;
    cb();
  }, 400);
}
public handleSingleClick() {
  console.log("one click");
}
public handleDoubleClick() {
  console.log("double!");
}
mhodges
  • 10,938
  • 2
  • 28
  • 46
  • 1
    @Przemo Is there a specific reason you *need* to use Rxjs? I didn't get that out of the original question. I thought you were trying to solve the problem with Rxjs, not that it was a requirement. Can you edit your question shed some more light on your requirements? – mhodges Sep 05 '18 at 21:17
  • 1
    I decided to use your solution. Thx! – Przemo Sep 07 '18 at 19:22
  • @Przemo Great =) Glad it worked for you. Please be sure to mark the answer as accepted for future viewers. Thank you! – mhodges Sep 10 '18 at 15:50
4

@Siddharth Ajmeras answer shows how to do this with events. I was not aware a dblclick event existed. The more you know. If you are still interested on how to do this with rxjs, here is an example.

// How fast does the user has to click
// so that it counts as double click
const doubleClickDuration = 100;

// Create a stream out of the mouse click event.
const leftClick$ = fromEvent(window, 'click')
// We are only interested in left clicks, so we filter the result down
  .pipe(filter((event: any) => event.button === 0));

// We have two things to consider in order to detect single or
// or double clicks.

// 1. We debounce the event. The event will only be forwared 
// once enough time has passed to be sure we only have a single click
const debounce$ = leftClick$
  .pipe(debounceTime(doubleClickDuration));

// 2. We also want to abort once two clicks have come in.
const clickLimit$ = leftClick$
  .pipe(
    bufferCount(2),
  );


// Now we combine those two. The gate will emit once we have 
// either waited enough to be sure its a single click or
// two clicks have passed throug
const bufferGate$ = race(debounce$, clickLimit$)
  .pipe(
    // We are only interested in the first event. After that
    // we want to restart.
    first(),
    repeat(),
  );

// Now we can buffer the original click stream until our
// buffer gate triggers.
leftClick$
  .pipe(
    buffer(bufferGate$),
    // Here we map the buffered events into the length of the buffer
    // If the user clicked once, the buffer is 1. If he clicked twice it is 2
    map(clicks => clicks.length),
  ).subscribe(clicks => console.log('CLicks', clicks));
tom van green
  • 1,674
  • 10
  • 14
  • This should be the accepted answer: reactive, pure, composable into other observable pipelines. Great job! – ghis Feb 21 '22 at 11:33
0

This is either what you want or very close to it:

import { Component, ViewChild, ElementRef } from '@angular/core';
import { fromEvent } from 'rxjs';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})

export class AppComponent  {

  @ViewChild('mybutton') button: ElementRef;

  ngAfterViewInit() {
    fromEvent(this.button.nativeElement, 'dblclick')
    .subscribe(e => console.log('double click'));

    fromEvent(this.button.nativeElement, 'click')
    .subscribe((e: MouseEvent) => {
      if (e.detail === 1) console.log('one click') // could use a filter inside a pipe instead of using an if statement
      // if (e.detail === 2) console.log('double click') // an alternative for handling double clicks
    });
  }
}

and the HTML:

<button #mybutton>Test</button>

This solution uses event.detail and is loosely based on this useful post - Prevent click event from firing when dblclick event fires - which not only discusses event.detail but also looks at the timing issues involved.

One of the big probs with your code is that you are subscribing to the event inside a function that is called multiple times, which means each time the button is clicked you create another subscription. Using ngAfterViewInit (which is only called once as part of the lifecycle) prevents this issue (and ensures the DOM is loaded).

Here's a stackblitz for you:

https://stackblitz.com/edit/angular-t6cvjj

Forgive me for my sloppy type declarations!

This satisfies your requirements as follows:

  • If I click once, I need to see only one message ('one click') - PASS
  • If I click twice (doubleclick), still I need to see only one message ('double click') - FAIL, you see 'one click' on the 1st click and 'double click' on the 2nd click
  • If I click more than twice - nothing happens - PASS

Due to the timing issues discussed in the SO post given, how you solve this is a matter of preference and thus I decided not to tackle it. And plus, you are not paying me ;) This answer however should set you well on your way to solving it fully.

PS There is a comment on the SO post above that suggests event.detail may not work on IE11

R. Richards
  • 24,603
  • 10
  • 64
  • 64
danday74
  • 52,471
  • 49
  • 232
  • 283