How can two event handlers coexist?


#1

I have an app where a route mixin handles a common event. On the other hand, some routes may have their own handler for this event.

I am trying to see how can I make both handlers coexist when the mixin is used in one of these routes.

I have put together a Twiddle that shows the problem:

https://ember-twiddle.com/7cceb249eb00a3b274aa

My use case is handling willTransition. There’s an element of navigation that should hide when certain transitions occur.

However, in one of these routes I am using willTransition to check if an object in a form has been successfuly saved or should be discarded, as I describe at Issue with Ember Data and Creating a Record


#2

I think you should be able to call this._super() in your route action which will ensure that the action you’re overwriting still gets called. So:

export default Ember.Route.extend(MyMixin, {
  actions: {
    myEvent() {
      console.log('handled by route');
      this._super();
      return true;
    }
  },
});

Does that work?


#3

It does the trick, actually. Thank you very much. I will use that, at least for now.

However, it leaves me a bit unfulfilled. It would mean that mixins can turn into not-so-black boxes. Users of these would need to know details of what’s going on there in order to use them effectively.

Also, there’s something that is confusing me a bit. I would have sworn that, on my twiddle, the mixin handler was winning out of the two. However now I run it and it’s the route handler. It may have been a confusion on my part, but leads me to wonder if the order in which things are loaded is not guaranteed? But that would not make sense. It’s the product of an extend, so the last definition should win out, making it the handler in the route to prevail.

But I digress…


#4

When you use extend, it’s whatever is the last definition that wins out…which is what’s happening in your case. It pretty sure it is a guarantee. :smile:

But, can you elaborate further on what you mean by “mixins can turn into no-so-black boxes”? I think that’s true, but I’m not clear on why that’s necessarily a bad thing. If you look at Ember’s View, for example, it’s really just composed of 12 mixins.

Is there maybe something you’re trying to do with Mixins that is leading you to this question that’s not quite doing what you’re expecting it to do?


#5

Sure. I would have expected mixins to be a bit more like black boxes, or at least attempt to. If I have a mixin, and I use it on my code, I would have thought I didn’t need to think “oh, but now I have to do this._super() here because MyMixin handles this action too”.

The case of Ember.View that you mention exposes a new problem: if I use several mixins together, now I have to start thinking about what handlers they may implement, whether there’s a correct order for their inclusion, and even if they can be used together at all. Of course, it’s ok that a given pair of mixins are incompatible, but this limitation adds what I think is an additional, artificial constraint.

Of course, my use case may be wrong, and I should be addressing it differently. I’ll explain it again in more detail.

I have a hamburger menu. You click on it and links to different sections appear. When you click on one of these links, the menu should close and the user should be led to the selected section.

I’m implementing it as follows. The menu belongs in the Application route, and shows/hides depending on the value of a property showingNav:

# app/routes/application.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
    return Ember.Object.create({
      showingNav: false,
    });
  },

  actions: {
    closeNav() {
      const model = this.modelFor('application');
      model.set('showingNav', false);
    },

    openNav() {
      const model = this.modelFor('application');
      model.set('showingNav', true);
    },
  }
});

The closeNav action is triggered on willTransition of each individual route. Since that’s something I’ll have to add to each affected route, I thought of putting it on a mixin:

# app/routes/navigable-route.js
import Ember from 'ember';

export default Ember.Mixin.create({
  actions: {
    willTransition() {
      this.send('closeNav');
    },
  },
});

But there are routes that have their own reasons for handling willTransition. To be specific, this is as explained Issue with Ember Data and Creating a Record for which, incidentally, I’d also love some feedback!

So now I am using the mixin, but I have to be aware that it relies on willTransition, and I have to explicitly say this._super() where appropriate:

import Ember from 'ember';
import NavigableRoute from 'my-app/routes/navigable-route';

export default Ember.Route.extend(NavigableRoute, {
  model() {
    return this.store.createRecord('player');
  },

  actions: {
    save() {
      const record = this.modelFor('players.new');
      record.save()
        .then(() => {
          this.transitionTo('root.show');
        })
        .catch(error => {
          console.error("Error saving player", error);
        });
    },

    willTransition() {
      this._super();
      const record = this.controllerFor('players.new').get('model');
      if (record.get('isNew')) {
        return record.destroyRecord();
      }
    },
  },
});

Months later, I come back and I make a change to the mixin. Maybe there’s been a deprecation, or I have found a better way to implement it. Now it doesn’t require willPaginate, but something else. I make the change and now I have those this._super() calls scattered around that serve no purpose, and may confuse developers to come later, wondering why that code is there in the first place. Or the new implementation relies on a different action, and now I have to go to the routes that use it, and make sure that there isn’t a new interference.

And hence my hoping that mixins were be black boxes.

I found something that would be helpful at http://stackoverflow.com/questions/22163263/multiple-mixins-with-same-events-in-ember-js . It would work like this:

# app/routes/navigable-route.js
import Ember from 'ember';

export default Ember.Mixin.create({
  sendCloseNav: Ember.on('willTransition', function() {
    this.send('closeNav');
  }),
});

That appears to promise to do that I ask, attaching several actions to the same event, same as several DOM event handlers can be respond to the same trigger together. But it doesn’t work! It’s from early 2014, so I’m going to assume it’s deprecated functionality of some sort, or route events work differently, or something like that.

All right, I hope that makes sense? Thank you for your interest, and for reading this far.


#6

Ah — yes, I think you stumbled onto the answer (the Ember.on('willTransition', function() {...}) solution). But, since it doesn’t work, you might be on a version of Ember before Ember.Evented was created…I have no idea when that came about.

Otherwise, this._super() is probably your best bet unfortunately.


#7

Re: Ember.Evented: not exactly. I’m on Ember 2.1, so it’s not a problem of it not being available. My working theory is that actions do not work the same way as events from Ember.Evented, and as a result cannot be handler with Ember.on.

I put together a twiddle demonstrating this:

https://ember-twiddle.com/89c4acafe29a7ebecd4c

This twiddle tries to handle an action using Ember.on, but the handling code is never called. A normal action handler (commented out on the route) works fine though.


#8

Oh — sorry, I misread that “it’s from 2014” to mean that you’re on a version of Ember from 2014. :wink:

I see. I suppose you could reopen the controller and route classes to add this behavior in an initializer? Something like:

(ps: haven’t tested this)

// app/initializers/evented-actions.js
import Ember from 'ember';

export function initialize() {
  const eventedSend = {
    send(actionName, ...args) {
      this._super(actionName, ...args);
      this.trigger(actionName, ...args);
    }
  };

  Ember.Route.reopen(eventedSend);
  Ember.Controller.reopen(eventedSend);
}
  
export default {
  name: 'evented-actions',
  initialize: initialize
};

Then, this gives you the Ember.on(actionName) for actions that would otherwise be in the actions hash. Something feels off about this solution, but I’m not quite sure what. Hope it might help!

(Relevant file: https://github.com/emberjs/ember.js/blob/v2.2.0/packages/ember-runtime/lib/mixins/action_handler.js#L175)