1

I currently have a JavaScript component which triggers a DOM event using JQuery like so:

$(document).trigger('myevent', mydata);

Other components register an event handler and update themselves accordingly when receiving the event.

Now, I want to extend this functionality in such a way, that any of the registered components should have the chance to prevent those updates (i.e., reject the event).

I thought of having two events instead:

$(document).trigger('myevent-before', mydata);
if( ??? )
    $(document).trigger('myevent', mydata);

Components which like to reject the update would register for the myevent-before and return some kind of information in order to prevent the triggering side from triggering the actual myevent.

Is this the way to go?

  • If yes, how to formulate the if clause? Does trigger() return some indicator if any of the called event handlers returned false?
  • If no, what's the way to do such a thing in JavaScript?

Example 1:

  1. Main Component: Hey, does anyone have a problem with an upcoming update?
  2. Component A: No.
  3. Component B: No.
  4. Main Component: OK, then I'll trigger the actual update!

Example 2:

  1. Main Component: Hey, does anyone have a problem with an upcoming update?
  2. Component A: No.
  3. Component B: Yes.
  4. Main Component: Oh, ok, no update it is.

(note that there might be more than 2 components registered of course)

D.R.
  • 20,268
  • 21
  • 102
  • 205
  • Then the answer is no, `trigger()` is like most jQuery methods chainable, and returns the collection, and that's it. – adeneo Oct 14 '16 at 15:22
  • 2
    It sounds like a XY problem to me. Could you better explain what is your expected behaviour with minimalistic sample? – A. Wolff Oct 14 '16 at 15:23
  • ^ I was just about to write that, why would you need to know this, it shouldn't matter what an event handler returns to whatever triggered the event ? – adeneo Oct 14 '16 at 15:24
  • I've clarified my question and added two examples of how I imagine the component interaction. – D.R. Oct 14 '16 at 15:27
  • Adding handlers to the bubbling order will solve your problems – winhowes Oct 14 '16 at 15:31
  • Still not clear to me. Are these events custom ones? What about a code snippet/jsFiddle? – A. Wolff Oct 14 '16 at 15:32
  • @A.Wolff: Yep, those events are custom ones. – D.R. Oct 14 '16 at 15:33
  • @winhowes: Isn't it some kind of bad practice to hope that all components have registered in the correct order? Anyone who wants to prevent the update would need to make sure to be registered *before* any other component? – D.R. Oct 14 '16 at 15:34
  • @D.R. So by `upcoming update` are you refering to any async request or what? Again, if you provide a MCVE on jsFiddle, it would be easier to help – A. Wolff Oct 14 '16 at 15:35
  • I'm going to create a jsFiddle.. – D.R. Oct 14 '16 at 15:41
  • @A.Wolff: jsFiddle: https://jsfiddle.net/t7cwesLd/2/ If the text input's value is `1` I would like to prevent the main component from triggering the `makered` event. – D.R. Oct 14 '16 at 15:47
  • @D.R. If your goal is to return from handler, then this is not possible. But you can set any variable and check for it before triggering any other events. What i don't really get is the logic behind your code. – A. Wolff Oct 14 '16 at 16:56
  • @A.Wolff: I could use a global variable, for sure, however, I'm trying to avoid any global variables by using self-contained components which should communicate via events only. – D.R. Oct 14 '16 at 17:46
  • @A.Wolff: What do you think about my solution below? Do you think its valid? Would love some feedback from a pro! – D.R. Oct 14 '16 at 17:52
  • @D.R. Events bubble so it doesn't matter what order you register in so long as it's different elements – winhowes Oct 16 '16 at 07:35

3 Answers3

1

It is possible to do this with plain Javascript. Not sure if it's possible with jQuery as well.

var input = document.getElementById( 'input' );
var makered = document.getElementById( 'makered' );

makered.addEventListener( 'click', function( e ) {
  var beforeMakeRedEvent = new Event( 'before-make-red', { cancelable: true } );
  input.dispatchEvent( beforeMakeRedEvent );
  if( beforeMakeRedEvent.defaultPrevented ) {
    console.log( 'Sorry, unable to make it red!' );
  }
  else {
    console.log( 'Making it red! Yeeehaawww!' );
  }
} );

input.addEventListener( 'before-make-red', function( e) {
  if( this.value == 1 ) {
    e.preventDefault();  
  }
} );
<input id="input" value="5">
<button id="makered">Make red</button>

Here's the above example translated to jQuery:

$( '#makered' ).on( 'click', function( e ) {
  var beforeMakeRedEvent = jQuery.Event( 'before-make-red' );
  $( '#input' ).trigger( beforeMakeRedEvent );
  if( beforeMakeRedEvent.isDefaultPrevented() ) {
    console.log( 'Sorry, unable to make it red!' );
  }
  else {
    console.log( 'Making it red! Yeeehaawww!' );
  }
} );

$( '#input' ).on( 'before-make-red', function( e ) {
  if( this.value == 1 ) {
    e.preventDefault();
  }
} );
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<input id="input" value="5">
<button id="makered">Make red</button>

Let's say, for the sake of argument, that you wanted to pass information about why some component wants to prevent a default action of an event. Then you could implement it like this. Whichever component is served first will immediately stop further propagation of the event, preventing other handlers from overwriting the additional information (I could think of a more robust system, but this is just an example of possibilities):

/* main.js */
$( '#makered' ).on( 'click', function( e ) {
  var beforeMakeRedEvent = jQuery.Event( 'before-make-red' );
  $( '#input' ).trigger( beforeMakeRedEvent );
  if( beforeMakeRedEvent.isDefaultPrevented() ) {
    // include the reason it was prevented
    console.log( 'Sorry, unable to make it red!', beforeMakeRedEvent.preventReason );
  }
  else {
    console.log( 'Making it red! Yeeehaawww!' );
  }
} );

/* component1.js */
$( '#input' ).on( 'before-make-red', function( e ) {
  if( this.value == 1 ) {
    e.preventDefault();
    
    // this is just a custom ad hoc property
    e.preventReason = 'I don\'t like the number 1!';
    
    // If I prevented it first, stop further propagation
    // to prevent others from fiddling with the event
    
    // try to leave this out, to see what happens
    e.stopImmediatePropagation();
  }
} );

/* component2.js */
$( '#input' ).on( 'before-make-red', function( e ) {
  if( this.value == 1 ) {
    e.preventDefault();
    
    // this is just a custom ad hoc property
    e.preventReason = 'I don\'t like the number 1 either!';
    
    // If I prevented it first, stop further propagation
    // to prevent others from fiddling with the event
    
    // leaving this out won't do much in this example
    e.stopImmediatePropagation();
  }
} );
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<input id="input" value="5">
<button id="makered">Make red</button>
Decent Dabbler
  • 22,532
  • 8
  • 74
  • 106
  • This is what I'm looking for - upvoted. Still, I'll mark the other answer as accepted as it makes use of JQuery, which is what I want to use for symmetry reasons. – D.R. Oct 14 '16 at 18:15
  • Coming right up. :) I just experimented with it and have a solution for you. – Decent Dabbler Oct 14 '16 at 18:18
  • Would you use `.isDefaultPrevented()` even if one could offer a different callback (see my solution below)? Is it semantically correct to "reuse" this method for my custom events? – D.R. Oct 14 '16 at 18:22
  • @D.R. Sorry, I misread your comment. I thought you said you'd mark my answer as accepted if it contained a jQuery example. Didn't realize you had figured out a solution on your own already. Still, I think my example is the more "proper" way to go about preventing actions in the realm of event dispatching. – Decent Dabbler Oct 14 '16 at 18:26
  • @D.R. Sorry, I'm not entirely sure what you are asking. Could you clarify what you mean? – Decent Dabbler Oct 14 '16 at 18:29
  • 1
    @D.R. I think I understand what you are asking now. Yes, I would go for the `.isDefaultPrevented()` solution, because it appears that in your example, you are merely passing a callback that the dispatching function is already aware of. Unless perhaps the callback accepts custom arguments as well, which depend on the component that will be preventing the action. – Decent Dabbler Oct 14 '16 at 18:32
  • 1
    @D.R. Thanks D.R.! I've added a more elaborate use case, if you need the components to provide feedback about why they prevented the action. It's just one way to implement it, but I hope it is of use to you, in illustrating what is possible with event dispatching. – Decent Dabbler Oct 14 '16 at 18:50
0

I think that the question should be described upside down: component A and component B might tell Main component what are their particular state regarding updates (true if updates can be done, false otherwise). This can be controlled via events or methods exposed by Main component to hold their individual state, and components A and B can call these methods to set their particular state to false when they fall into a specific condition (one that prevents them to be updated) and reset that status to true after this preventing condition has ended.

So, when a new updating cycle starts at Main component, the first thing it has to do is to iterate on the status array to check if some of the components status is set to false. If this is the case, the update stops. If all components status are set to true, update goes on.

  • This would be a possible solution, however, the number of state changes in the components is much higher than the number of events triggered by the main component. So the "main components pulls information" approach is a performance-critical detail. – D.R. Oct 14 '16 at 17:45
0

Update 2: Used JQuery-based solution of @DecentDabbler, which is more idiomatic.


Update 1: .trigger() is synchronous, no need for .when(). A short and simple way is:

// main.js:
var maySend = true;
var prevent = function() { maySend = false; };
$(document).trigger('myevent-before', [prevent]);
if(maySend) {
    $(document).trigger('myevent');
}

// component.js:
$(document).on('myevent-before', function(e, prevent) {
    if(some_condition) {
        prevent();
    }
});

Old solution using .when():

After searching the web for many hours, I've found a solution based upon JQuery's .when() method:

// main.js:
var maySend = true;
var prevent = function() { maySend = false; };
$.when($(document).trigger('myevent-before', [prevent])).done(function(){
    if(maySend) {
        $(document).trigger('myevent');
    }
});

// component.js:
$(document).on('myevent-before', function(e, prevent) {
    if(some_condition) {
        prevent();
    }
});
Community
  • 1
  • 1
D.R.
  • 20,268
  • 21
  • 102
  • 205
  • `$.when` accepts promise(s). Here you don't pass promise but jq object. But because `trigger()` is sync, it doesn't really matter. Now funny part is that in link you provided, there is i guess the solution in comment, use `triggerHandler()` which let you return from handler. see e.g: https://jsfiddle.net/arvw9qhw/ – A. Wolff Oct 14 '16 at 18:04
  • @A.Wolff: Your suggestion does not work for multiple registered handlers: https://jsfiddle.net/arvw9qhw/1/. – D.R. Oct 14 '16 at 18:17
  • @A.Wolff: But, I guess my answer here "just works" even without `$.when`. – D.R. Oct 14 '16 at 18:19