86

When a user clicks a certain link I would like to present them with a confirmation dialog. If they click "Yes" I would like to continue the original navigation. One catch: my confirmation dialog is implemented by returning a jQuery.Deferred object which is resolved only when/if the user clicks the Yes button. So basically the confirmation dialog is asynchronous.

So basically I want something like this:

$('a.my-link').click(function(e) {
  e.preventDefault(); e.stopPropogation();
  MyApp.confirm("Are you sure you want to navigate away?")
    .done(function() {
      //continue propogation of e
    })
})

Of course I could set a flag and re-trigger click but that is messy as heck. Any natural way of doing this?

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
George Mauer
  • 117,483
  • 131
  • 382
  • 612
  • 1
    Have you tried `$this.trigger(e);` (*edit* that would need a flag if it worked) or `$this.parent().trigger(e)` (in the callback), where `$this` refers to the clicked elements? – Felix Kling Oct 18 '11 at 18:33
  • @FelixKling That won't work because `e.preventDefault(); e.stopPropogation();` has sent flags on e. If there was some way to take a copy of e to e2 then I could call something along the lines of `e.handler(e2)` which I think would work. – George Mauer Oct 18 '11 at 18:36
  • Well, I thought maybe jQuery is smart enough and resets the flags when it is passed to `trigger`... – Felix Kling Oct 18 '11 at 18:37
  • @FelixKling good call, but I just checked the code - no such luck. – George Mauer Oct 18 '11 at 18:45
  • @FelixKling: I just tried your `.parent()` solution using the original event object, and it seems to work fine. http://jsfiddle.net/vTzDM/ I wonder why it wasn't working for George. – user113716 Oct 18 '11 at 20:04
  • @Ӫ_._Ӫ: I guess he tried `.trigger(e)` (which does not work). In your case, the event object is just passed as parameter to the event handler but its properties are not used for the new event object, so passing it does not seem to be necessary. I don't know whether the OP needs it or not. `.parent().click()` seems to be a good alternative, but it depends on what other event handlers in hierarchy are doing. Thanks for trying :) – Felix Kling Oct 18 '11 at 20:10
  • @FelixKling: Ah, you're right. I have it wrong in my answer too. – user113716 Oct 18 '11 at 20:19
  • @Ӫ_._Ӫ: I think your second and third suggestion are actually good. – Felix Kling Oct 18 '11 at 20:21
  • 1
    @FelixKling: Thanks, but if the original event object isn't needed then `.parent().click()` would be by far the simplest. – user113716 Oct 18 '11 at 20:25

8 Answers8

18

Below are the bits from the code that actually worked in Chrome 13, to my surprise.

function handler (evt ) {
    var t = evt.target;
    ...
    setTimeout( function() {
        t.dispatchEvent( evt )
    }, 1000);
    return false;
}

This is not very cross-browser, and maybe will be fixed in future, because it feels like security risk, imho.

And i don't know what happens, if you cancel event propagation.

c69
  • 19,951
  • 7
  • 52
  • 82
8

It could be risky but seems to work at the time of writing at least, we're using it in production.

This is ES6 and React, I have tested and found it working for the below browsers. One bonus is if there is an exception (had a couple during the way making this), it goes to the link like a normal <a> link, but it won't be SPA then ofc.

Desktop:

  • Chrome v.76.0.3809.132
  • Safari v.12.1.2
  • Firefox Quantum v.69.0.1
  • Edge 18
  • Edge 17
  • IE11

Mobile/Tablet:

  • Android v.8 Samsung Internet
  • Android v.8 Chrome
  • Android v.9 Chrome
  • iOs11.4 Safari
  • iOs12.1 Safari

.

import 'mdn-polyfills/MouseEvent'; // for IE11
import React, { Component } from 'react';
import { Link } from 'react-router-dom';

class ProductListLink extends Component {
  constructor(props) {
    super(props);
    this.realClick = true;

    this.onProductClick = this.onProductClick.bind(this);
  }

  onProductClick = (e) => {
    const { target, nativeEvent } = e;
    const clonedNativeEvent = new MouseEvent('click', nativeEvent);

    if (!this.realClick) {
      this.realClick = true;
      return;
    }

    e.preventDefault();
    e.stopPropagation();

    // @todo what you want before the link is acted on here

    this.realClick = false;
    target.dispatchEvent(clonedNativeEvent);
  };

  render() {
    <Link
      onClick={(e => this.onProductClick(e))}
    >
      Lorem
    </Link>  
  }
}
OZZIE
  • 6,609
  • 7
  • 55
  • 59
  • Interesting, so you re-dispatch on the original target. Unfortunately that means anyone lower down the ui tree would have to know to check for `realClick` which might be unrealistic when talking wit third party dependencies. Also I wonder if this only works due to the specific way react handles events. Any way, its a fine idea for a specific use case – George Mauer Sep 24 '19 at 13:22
  • @GeorgeMauer I don't stopPropagation or prevent default after it's redispatched so I think it should be safe as well. Unless dispatchEvent doesn't propagate for some reason. – OZZIE Sep 26 '19 at 06:49
  • Just wanted to say that I have this running in production without issues for months now :-) But do consider if you add for example GA/Analtytics tracking that some users block GA/Analytics and then calling methods on that might throw errors then and possibly break the link from working. In that case simply try-catch (only) that statement. – OZZIE Aug 28 '20 at 11:31
  • In React 16 (Typescript) this gives me an error `react__WEBPACK_IMPORTED_MODULE_0__.MouseEvent is not a constructor` in React 16 ... any ideas? – Sasgorilla Jul 17 '21 at 16:55
  • Are you importing MouseEvent from somewhere? – Ben Taliadoros May 24 '23 at 12:41
3

I solved problem by this way on one of my projects. This example works with some basic event handling like clicks etc. Handler for confirmation must be first handler bound.

    // This example assumes clickFunction is first event handled.
    //
    // you have to preserve called function handler to ignore it 
    // when you continue calling.
    //
    // store it in object to preserve function reference     
    var ignoredHandler = {
        fn: false
    };

    // function which will continues processing        
    var go = function(e, el){
        // process href
        var href = $(el).attr('href');
        if (href) {
             window.location = href;
        }

        // process events
        var events = $(el).data('events');

        for (prop in events) {
            if (events.hasOwnProperty(prop)) {
                var event = events[prop];
                $.each(event, function(idx, handler){
                    // do not run for clickFunction
                    if (ignoredHandler.fn != handler.handler) {
                        handler.handler.call(el, e);
                    }
                });
            }
        }
    }

    // click handler
    var clickFunction = function(e){
        e.preventDefault();
        e.stopImmediatePropagation();
        MyApp.confirm("Are you sure you want to navigate away?")
           .done(go.apply(this, e));
    };

    // preserve ignored handler
    ignoredHandler.fn = clickFunction;
    $('.confirmable').click(clickFunction);

    // a little bit longer but it works :)
Jan Míšek
  • 1,647
  • 1
  • 16
  • 22
  • For the record, in more recent versions of jquery `$(el).data('events')` has been removed. There is an unofficial and unsupported way to get events by doing `$._data(el, 'events')` note that el must be a DOM object and not jquery – George Mauer Dec 18 '13 at 15:19
  • Amazing solution, I was not aware of stopImmediatePropagation. Thanks! – Diego Fortes May 18 '19 at 00:34
2

If I am understanding the problem correctly, I think you can just update the event to be the original event in that closure you have there. So just set e = e.originalEvent in the .done function.

https://jsfiddle.net/oyetxu54/

MyApp.confirm("confirmation?")
.done(function(){ e = e.originalEvent;})

here is a fiddle with a different example (keep the console open so you can see the messages): this worked for me in chrome and firefox

user2977636
  • 2,086
  • 2
  • 17
  • 21
  • 1
    Your fiddle doesn't work. Even if 'stop' is clicked, the event is propagated. Tested on the latest chrome – Yoo Matsuo Apr 12 '17 at 02:35
0

I solved this by:

  1. placing a event listener on a parent element
  2. removing the class from the link ONLY when the user confirms
  3. reclicking the link after I have removed the class.

function async() {
  var dfd = $.Deferred();
  
  // simulate async
  setTimeout(function () {
    if (confirm('Stackoverflow FTW')) {
      dfd.resolve();
    } else {
      dfd.reject();
    }
  }, 0);
  
  return dfd.promise();
};

$('.container').on('click', '.another-page', function (e) {
  e.stopPropagation();
  e.preventDefault();
  async().done(function () {
    $(e.currentTarget).removeClass('another-page').click();
  });
});

$('body').on('click', function (e) {
  alert('navigating somewhere else woot!')
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="container">
  <a href="#" class="another-page">Somewhere else</a>
</div>

The reason I added the event listener to the parent and not the link itself is because the jQuery's on event will bind to the element until told otherwise. So even though the element does not have the class another-page it still has the event listener attached thus you have to take advantage of event delegation to solve this problem.

GOTCHAS this is very state based. i.e. if you need to ask the user EVERYTIME they click on a link you'll have to add a 2nd listener to readd the another-page class back on to the link. i.e.:

$('body').on('click', function (e) {
  $(e.currentTarget).addClass('another-page');
});

side note you could also remove the event listener on container if the user accepts, if you do this make sure you use namespace events because there might be other listeners on container you might inadvertently remove. see https://api.jquery.com/event.namespace/ for more details.

devkaoru
  • 1,142
  • 9
  • 7
0

We have a similar requirement in our project and this works for me. Tested in chrome and IE11.

$('a.my-link').click(function(e) {
  e.preventDefault(); 
  if (do_something === true) {
    e.stopPropogation();
    MyApp.confirm("Are you sure you want to navigate away?")
    .done(function() {
      do_something = false;
      // this allows user to navigate 
      $(e.target).click();
    })
  }

})
  • 1
    Note that this is not the same thing! It will also trigger *other* click handlers attached to the same element to fire again. In fact, if two use this same technique you'll get an infinite click loop. – George Mauer Aug 09 '17 at 14:39
0

I edited your code. New features that I added:

  1. Added namespace to event;
  2. After click on element event will be removed by namespace;
  3. Finally, after finish needed actions in "MyApp" section continue propagation by triggering others element "click" events.

Code:

$('a.my-link').on("click.myEvent", function(e) {
  var $that = $(this);
  $that.off("click.myEvent");
  e.preventDefault();
  e.stopImmediatePropagation();
  MyApp.confirm("Are you sure you want to navigate away?")
    .done(function() {
        //continue propogation of e
        $that.trigger("click");
    });
});
Yura Kosyak
  • 401
  • 3
  • 16
  • Hi, Please also explain your code when posting an answer - This will help understand it better – Ali Dec 14 '18 at 07:01
  • Show me the code! It's about something you said? But if that's what you want!? Okay. – Yura Kosyak Dec 14 '18 at 07:29
  • Hi, This is site rule to have some explanation of your code when posting answers - It is highly recommended in case if someone else also having the same issue and they come across this post. – Ali Dec 14 '18 at 07:42
  • This is kind off the thing I said I wanted to avoid though - you're just re-triggering another event. – George Mauer Dec 15 '18 at 02:02
-2

This is untested but might serve as a workaround for you

$('a.my-link').click(function(e) {
  e.preventDefault(); e.stopPropogation();
  MyApp.confirm("Are you sure you want to navigate away?")
    .done(function() {
      //continue propogation of e
      $(this).unbind('click').click()
  })
})
irishbuzz
  • 2,420
  • 1
  • 19
  • 16
  • This would unbind all click event handlers! Also this guard would only occur once so any client-side page caching is out the window. I have workarounds, just wondering if there is something more idiomatic. – George Mauer Oct 18 '11 at 18:55