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));
}),
});
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 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));
})
});
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'));
}),
});
You have two options:
Use a closure
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.
You can use the lib ember-click-outside. Worked for me.
I have an ArrayContoller on which I want to set a boolean property based on the properties of its contents.
Plain-language description of the logic:
If the array contains any items with a property of isRetired equal to true, set the retiredShoes property of the ArrayController to true, otherwise, set the ArrayController retiredShoes property to false.
It seems like this should be a simple matter, but I haven't found a solution anywhere, and I'm still pretty new at this.
I'll put together a jsfiddle if necessary.
Here are the controllers for the array and the object:
App.ApplicationController = Ember.ArrayController.extend({
sortProperties: ['title'],
itemController: 'shoe',
retiredShoes: function() {
//how do I compute this sucker?
}
});
App.ShoeController = Ember.ObjectController.extend({
needs: ['application'],
actions: {
delete: function() {
var shoe = this.get('model'),
runs = shoe.get('runs');
shoe.deleteRecord();
shoe.save();
},
toggleRetired: function() {
var shoe = this.get('model');
shoe.toggleProperty('isRetired');
shoe.save();
}
}
});
Off top of my head, without jsbin. If there's a problem/bug, drop me a comment and I'll look it over again.
App.ApplicationController = Ember.ArrayController.extend({
retiredShoes: function() {
return this.get("model").isAny("isRetired", true);
}.property("model.#each.isRetired")
});
I have the following code:
App.UserController = App.EditableController.extend({
needs: 'application',
test: function() {
return this.get('controller.application.me.email');
}.property('controller.application.me.email'),
});
App.ApplicationController = Ember.Controller.extend({
isPublic : true,
me : null,
init: function () {
this.set('me', App.User.find(1));
this._super();
}
});
But the computed property doesn't seem to update once the model loads (from the console):
> App.__container__.lookup('controller:Application').get('me.email')
"test#example.com"
> App.__container__.lookup('controller:User').get('test')
undefined
Am I missing something?
Assuming your App.EditableController is of type Ember.ObjectController then this should work:
App.UserController = Ember.ObjectController.extend({
needs: 'application',
// notice the we use here plural 'controllers' to have access to the
// controllers defined with the 'needs' API
contentBinding: 'controllers.application',
test: function() {
return this.get('content.me.email');
}.property('content')
});
In the case that your App.EditableControlleris of type Ember.Controller than this should do the job:
App.UserController = Ember.Controller.extend({
needs: 'application',
// notice the we use here plural 'controllers' to have access to the
// controllers defined with the 'needs' API
controllerBinding: 'controllers.application',
test: function() {
return this.get('controller.me.email');
}.property('controller')
});
Now doing App.__container__.lookup('controller:User').get('test') in the console should output:
"test#example.com" // or whatever your example mail is
Hope it helps.
App.Router.map(function() {
this.resource('documents', { path: '/documents' }, function() {
this.route('edit', { path: ':document_id/edit' });
});
this.resource('documentsFiltered', { path: '/documents/:type_id' }, function() {
this.route('edit', { path: ':document_id/edit' });
this.route('new');
});
});
And this controller with a subview event that basically transitions to a filtered document
App.DocumentsController = Ember.ArrayController.extend({
subview: function(context) {
Ember.run.next(this, function() {
//window.location.hash = '#/documents/'+context.id;
return this.transitionTo('documentsFiltered', context);
});
},
});
My problem is that this code works fine when Hash of page is changed.
But when I run the above code NOT w/ the location.hash bit and w/ the Ember native transitionTo I get a cryptic
Uncaught TypeError: Object [object Object] has no method 'slice'
Any clues?
Thanks
UPDATE:
App.DocumentsFilteredRoute = Ember.Route.extend({
model: function(params) {
return App.Document.find({type_id: params.type_id});
},
});
{{#collection contentBinding="documents" tagName="ul" class="content-nav"}}
<li {{action subview this}}>{{this.nameOfType}}</li>
{{/collection}}
The problem is that your model hook is returning an array, while in your transitionTo you are using a single object. As a rule of thumb your calls to transitionTo should pass the same data structure that is returned by your model hook. Following this rule of thumb i would recommend to do the following:
App.DocumentsController = Ember.ArrayController.extend({
subview: function(document) {
var documents = App.Document.find({type_id: document.get("typeId")});
Ember.run.next(this, function() {
return this.transitionTo('documentsFiltered', documents);
});
}
});
Note: I assume that the type_id is stored in the attribute typeId. Maybe you need to adapt it according to your needs.
I have a model of patient object
App.Router.map (function () {
this.resource('patients');
this.resource('patient', {path: ':patient_id'}, function(){
this.resource('dashboard', function() {
this.route('summary');
});
});
});
App.PatientRoute = Ember.Route.extend({
model: function(params) {
return App.Patient.find(params.patient_id);
},
setupController: function(){
console.log("Menu Items:" + App.PatientMenuItem.find() );
this.controllerFor('patient').set('menuItems', App.PatientMenuItem.find());
}
});
App.DashboardSummaryRoute = Ember.Route.extend({
setupController: function(){
this.controllerFor('dashboard.summary').set('content', this.controllerFor('patient').get('model'));
this.controllerFor('dashboard.summary').getPatient();
}
});
App.DashboardSummaryController = Ember.ObjectController.extend({
getPatient:function(){
console.log(this.content.get_allergies);
}
});
App.PatientController = Ember.ObjectController.extend({
menuItems:[],
});
<script type="text/x-handlebars" data-template-name="dashboard/summary">
Summary{{this.content.get_allergies}}
</script>
In the above I am not able to access the same get_allergies in DashboardSummaryController but I am able to access it in handlebars, Can anyone help me what is the mistake ?
Thanks in advance
I don't know if this alone solves the problem, but always use the get() and set() methods when accessing properties. So i would suggest to try this in your getPatient() method:
App.DashboardSummaryController = Ember.ObjectController.extend({
getPatient:function(){
console.log(this.get("content.get_allergies"));
}
});
Why does the template work? The Handlebars expression you have there is automatically translated into the call, i have suggested for your controller method.