4

I'm writing a Mixin to handle when user clicks outside of a view/component.

This is the mixin:

App.ClickElsewhereMixin = Ember.Mixin.create({

  onClickElsewhere: Ember.K,

  didRender: function() {
    this._super.apply(this, arguments);
    return $(document).on('click', this.get('onClickElsewhere'));
  },

  willDestroyElement: function() {
    this._super.apply(this, arguments);
    $(document).off('click', this.get('onClickElsewhere'));
  },
});

I use it in my component:

onClickElsewhere: function() {
    this.send('exitEditMode');
},

But when I run it, I get:

TypeError: this.send is not a function

How can I keep the this context?


Solution:

just to make it easier for the reader, here the working Mixin:

App.ClickElsewhereMixin = Ember.Mixin.create({

  onClickElsewhere: Ember.K,

  setupListener: Ember.on('didRender', function() {
    // Set an event that will be fired when user clicks outside of the component/view
    return $(document).on('click', $.proxy(this.get('onClickElsewhere'), this));
  }),

  removeListener: Ember.on('willDestroyElement', function() {
    // Clean the previously defined event to keep events stack clean
    return $(document).off('click', $.proxy(this.get('onClickElsewhere'), this));
  }),
});
Fez Vrasta
  • 14,110
  • 21
  • 98
  • 160

5 Answers5

7

The current answer doesn't check whether the click was actually outside of the element – a click on the component will also trigger the callback.

Here's an updated version:

export default Ember.Mixin.create({
  onOutsideClick: Ember.K,

  handleOutsideClick: function(event) {
    let $element = this.$();
    let $target = $(event.target);

    if (!$target.closest($element).length) {
      this.onOutsideClick();
    }
  },

  setupOutsideClickListener: Ember.on('didInsertElement', function() {
    let clickHandler = this.get('handleOutsideClick').bind(this);

    return Ember.$(document).on('click', clickHandler);
  }),

  removeOutsideClickListener: Ember.on('willDestroyElement', function() {
    let clickHandler = this.get('handleOutsideClick').bind(this);

    return Ember.$(document).off('click', clickHandler);
  })
});
Greg Funtusov
  • 1,377
  • 15
  • 18
  • So this will not actually remove the listener properly, doing `this.get(...).bind(this)` in each function creates references to *different* functions, so the `document.off(...)` won't identify the listener properly. It should store a reference to the bound function and *use that same reference* in both add/remove – kevlarr Jul 06 '17 at 21:22
6

Greg answer have a mistake, that makes removing the clickHandler event not working. Which means that your clickevent will fire even if you destroy the component.

Here is proper version

import Ember from 'ember';

export default Ember.Mixin.create({
    onOutsideClick: Ember.K,

  handleOutsideClick: function(event) {
    let $element = this.$();
    let $target = $(event.target);

    if (!$target.closest($element).length) {
      this.onOutsideClick();
    }
  },

  setupOutsideClickListener: Ember.on('didInsertElement', function() {

    let clickHandler = this.get('handleOutsideClick').bind(this);

    return Ember.$(document).on('click', clickHandler);
  }),

  removeOutsideClickListener: Ember.on('willDestroyElement', function() {

    let clickHandler = this.get('handleOutsideClick').bind(this);

    return Ember.$(document).off('click', Ember.run.cancel(this, clickHandler));
  })
});
klonodo
  • 198
  • 2
  • 9
  • Your solution worked for me thanks a lot. I think the problem was that I was not doing this.get('clickHandler') – Ayoub Nov 23 '16 at 14:12
4

The ember way of doing it is Ember.run.bind. This takes care of binding and the run loop.

App.ClickElsewhereMixin = Ember.Mixin.create({

  onClickElsewhere: Ember.K,

  setupListener: Ember.on('didRender', function() {
    this.set('clickHandler', Ember.run.bind(this, this.onClickElsewhere));
    Ember.$(document).click(this.get('clickHandler'));
  }),

  removeListener: Ember.on('willDestroyElement', function() {
    Ember.$(document).off('click', this.get('clickHandler'));
  }),
});
pol
  • 161
  • 6
2

You have two options:

  1. Use a closure
  2. Use bind

Closure

App.ClickElsewhereMixin = Ember.Mixin.create({

  onClickElsewhere: Ember.K,

  didRender: function() {
    this._super.apply(this, arguments);
    return $(document).on('click', function(this){ return this.get('onClickElsewhere'); }(this));
  },

  willDestroyElement: function() {
    this._super.apply(this, arguments);
    $(document).off('click', function(this){ return this.get('onClickElsewhere'); }(this));
  },
});

Bind

App.ClickElsewhereMixin = Ember.Mixin.create({

  onClickElsewhere: Ember.K,

  didRender: function() {
    this._super.apply(this, arguments);
    return $(document).on('click', this.get('onClickElsewhere').bind(this));
  },

  willDestroyElement: function() {
    this._super.apply(this, arguments);
    $(document).off('click', this.get('onClickElsewhere').bind(this));
  },
});

However, not all browsers support bind yet.

Also, I think you need to use sendAction instead of send in the component (http://guides.emberjs.com/v1.10.0/components/sending-actions-from-components-to-your-application/)

Edit:

jQuery.proxy uses call/apply underneath the covers. See this post for a discussion of call/apply vs bind.

Community
  • 1
  • 1
lsowen
  • 3,728
  • 1
  • 21
  • 23
2

You can use the lib ember-click-outside. Worked for me.

William Weckl
  • 2,435
  • 4
  • 26
  • 43