I have a mixin which automatically recalculates and sets the height of a div on page resize.
It works but it seems silly to me to be binding to a jQuery event and triggering an Ember event manually every time it is called.
Is there a way to bind to window events directly in Ember?
I have a simplified JSFiddle here
This is the code:
App.windowWrapper = Ember.Object.create(Ember.Evented,
resizeTimer: null
init: ->
#_super()
object = this
$(window).on 'resize', ->
window.clearTimeout(object.resizeTimer)
object.resizeTimer = setTimeout( App.windowWrapper.resize, 100)
true
resize: ->
App.windowWrapper.fire('resize')
)
And the mixin that is calling it.
App.Scrollable = Ember.Mixin.create
classNames: "scrollable"
init: ->
Ember.assert("Scrollable must be mixed in to a View", this instanceof Ember.View)
#_super()
didInsertElement: ->
#_super()
#calculateHeight()
App.windowWrapper.on('resize', this, 'calculateHeight')
willDestroyElement: ->
App.windowWrapper.off('resize', this, 'calculateHeight')
#_super()
calculateHeight: ->
offset = #$().offset().top
windowHeight = $(window).height()
#$().css('height', windowHeight - offset)
I know I'm a few years late, but I was looking for a solution to the same problem and found this:
jQuery(window).on('resize', Ember.run.bind(this, this.handleResize));
(source: Ember.js ofiicial)
**** P.S.
About the implementation of this little trick - I don't know if it's possible to use it in a controller method and trigger on init (as denis.peplin commented), I'm actually binding the event in the route's 'setupController' function. I don't know if it's best-practice, but it works like a charm in my app.
There's nothing wrong with registering your own event handler with jQuery for something like this.
Ember doesn't have any window event handling currently.
I'd also suggest trying to come up with a solution that didn't rely on globals.
And sample code for Itay Hamashmid's answer:
App.ApplicationController = Ember.Controller.extend({
handleResize: function() {
console.log('resized');
},
bindResizeEvent: function() {
jQuery(window).on('resize', Ember.run.bind(this, this.handleResize));
}.on('init')
});
I don't think it's possible with Ember.js - the only place where it provides built in DOM event handling is for Ember.View (click etc.) and maybe in other view related classes.
We also used similar approaches to your solution to handle special cases like this!
For the ones who searching by newest answers or just get rid of jquery...
In my case I just want to know if window resized to calc width of element.
import Component from '#ember/component';
export default Component.extend({
size: window.innerWidth,
init() {
this._super(...arguments);
window.addEventListener('resize', ()=> this.set('size', window.innerWidth));
}
});
Yeah, I know there is no way to unbind this kind of anonymous function, but I've searched and I can't find good answers...
I thinking to refactor this to use jquery but in a restricted scope like:
import Component from '#ember/component';
import $ from 'jquery';
export default Component.extend({
size: window.innerWidth,
init() {
this._super(...arguments);
$(document.querySelector('#something'))
.on('resize', (e)=> this.set('size', e.target.innerWidth));
}
});
And this will listener will live until this component lives!
Related
I'm new to Ember.js and I've got some problems to understand its philosophy. I know actions up, data down but in real life, lets say I have Fotorama initialized in my-gallery component (I don't know if that is ok, but I did it in didInsertElement method). This library has its own events. They could look like this in plain JS:
$('.fotorama').on('fotorama:ready', function (e, fotorama) {});
or:
$('.fotorama').on('fotorama:show', function () {});
but I feel in Ember, those should be somehow mapped into actions in component.
My question is: how? I need to fire some actions (to be catched by another components or maybe a router) inside those actions. So I think it should be like this: this.sendAction('actionName', actionParams);
You can keep component reference to call sendAction method.
didInsertElement(){
this._super(...arguments);
var _this=this;
this.$('.fotorama').on('fotorama:show', function () {
_this.sendAction('actionName', actionParams);
});
}
willDestroyElement(){
this._super(...arguments);
this.$('.fotorama').off('fotorama:show')
}
If we find an better answer to this question. I will happily remove my answer.
I have a similar problem. I need to have a third party library talk to my ember app so I registered a custom event in my ember app:
const App = Ember.Application.extend({
customEvents: {
foo: 'foo',
},
});
Then triggered it from third party code:
<a href="..." onclick="$(this).trigger('foo'); return false;">
And, in a component, write the handler for that event according to the docs:
export default Ember.Component.extend({
foo(event) {
console.warn('event happened', event);
},
});
See:
* https://guides.emberjs.com/v3.0.0/components/handling-events/
* https://www.emberjs.com/api/ember/release/classes/Application/properties/customEvents?anchor=customEvents
Views are gone since Ember 2.0.0, but you could do this:
// app/views/application.js or app/application/view.js
import Ember from 'ember';
export default Ember.Component.extend({
classNames: []
});
Since Ember CLI 2.7 this workaround no longer works, looks like the views folder is now being ignored. However, the Ember inspector still shows this for the application view:
view:foobar#view:toplevel
And the HTML is:
<div id="ember420" class="ember-view">
<h2>application</h2>
</div>
It still is a view, there must be a way to customize it.
You can use jQuery (via Ember.$) to manually add the class at some point in the application's startup. This will work:
// in app/routes/application.js
export default Ember.Route.extend({
// ...
activate() {
this._super(...arguments);
let root = Ember.getOwner(this).get('rootElement');
Ember.$(root).addClass('my-custom-class');
},
// ...
});
Since you are asking about the application route, there is no need to clean up after this in the deactivate hook.
This is a specialization of something I've done to facilitate tweaking of styles depending on the current route. Here's an instance initializer that will add a route--xyz class to the root element for the current route hierarchy:
import Ember from 'ember';
function toCssName(routeName) {
return `route--${routeName.dasherize().replace(/\./g, '-')}`;
}
export function initialize(appInstance) {
Ember.Route.reopen({
activate() {
this._super(...arguments);
Ember.$(appInstance.rootElement).addClass(toCssName(this.routeName));
},
deactivate() {
this._super(...arguments);
Ember.$(appInstance.rootElement).removeClass(toCssName(this.routeName));
}
});
}
export default {
name: 'css-route-name',
initialize
};
With this initializer, the root element will always have the class route--application in addition to any other active routes.
Problem can be solved via css:
body > .ember-view {
height: 100%;
}
As jquery selector 'body > .ember-view' should work too
Seems like the best option is to add a component to the application template. What makes it the best solution is that you don't need extra addons or hacks.
More context: https://github.com/emberjs/ember.js/issues/11486
// application template
{{#app-view-substitute}}
{{outlet}}
{{/app-view-substitute}}
Note: Not convenient for a large app where other developers have already made assumptions about the level of nesting of the elements. This adds one more level to every single element and CSS, even when carefully crafted, might break. An alternative is to: https://stackoverflow.com/a/40187809/7852
What I ended up doing is:
// top level component rendered in the application template
didInsertElement: function() {
this._super(...arguments);
this._addIdAndCSSClassToApplicationView();
},
_addIdAndCSSClassToApplicationView: function() {
let root = Ember.getOwner(this).get('rootElement'); // Ember >= 2.3
let applicationView = root.querySelector('.ember-view:first-child');
let idclass = 'myRequiredName';
applicationView.id = idclass;
let classes = applicationView.className;
applicationView.className = classes + ' ' + idclass;
}
Wrapping the outlet is cleaner, this is a hack, but it makes sense in a large app where other have already made assumptions about the levels of nesting in the app.
With Ember 2.2 I had to change the root line to something like this:
let root = Ember.$('.ember-application');
I am setting up a page where my user can add an orgLanguage, and I'd like to show a special message if this is the first orgLanguage being added. I'm able to get my code working, but it sure looks ugly, and I'm wondering if there's a better way to handle this?
First, here's my Handelbars template:
Handlebars Template (Simplified):
{{#if isFirstOrgLanguage}}
...display some content
{{/if}}
That variable is defined on my controller as follows.
Controller (Simplified):
export default Ember.ObjectController.extend({
isFirstOrgLanguage: function() {
// the 'orgLanguages' controller property is set in the route
var orgLanguagesPromiseArray = this.get('orgLanguages');
return orgLanguagesPromiseArray.then( function() {
var orgLanguagesRecordArray = orgLanguagesPromiseArray.get('content');
var orgLanguagesArray = orgLanguagesRecordArray.get('content');
return orgLanguagesArray ? orgLanguagesArray.length === 1 : true;
});
}.property('orgLanguages')
}
I've named my variables the data type that I receive. You'll note that this is a computed property that depends on a controller property set on my route, shown below.
Route (Simplified):
setupController: function (controller, model) {
this._super(controller, model);
controller.set('orgLanguages', this.store.find('org-language') );
},
Finally, I'd like to call some basic jQuery on this Handlebars template if isFirstOrgLanguage is true, so I set up my view as follows.
View:
export default Ember.View.extend({
didInsertElement: function() {
this.get('controller').get('isFirstOrgLanguage').then( function( isFirstOrgLanguage ) {
console.log('isFirstOrgLanguage', isFirstOrgLanguage);
});
}
});
This seems like a crazy amount of promises and async management just to answer the question "is there exactly 1 orgLanguage defined"? Although the above works, is there a simpler way, or perhaps "The Ember Way" to do this?
Update:
In doing some additional research, it seems this has been a topic for some debate. Here are relevant discussions I've seen on this. If I settle on a pattern I like, I'll post it as as an answer, but would welcome other suggestions.
http://discuss.emberjs.com/t/dashboard-type-views/5187/24
http://discuss.emberjs.com/t/the-right-way-to-load-additional-models-to-build-filtering-checkboxes/4966/4
I wanted to post how I eventually solved this.
First, it became clear that there are recommended solutions to this pattern, but no "one true way". See http://discuss.emberjs.com/t/the-right-way-to-load-additional-models-to-build-filtering-checkboxes/4966/4.
What I wound up using was this:
Route:
...
afterModel: function() {
var _this = this;
Ember.RSVP.hash({
languages: this.store.find('language'),
orgLanguages: this.store.find('org-language')
}).then( function( hash ) {
_this.set('controller.languages', hash.languages );
_this.set('controller.orgLanguages', hash.orgLanguages );
});
},
...
The key insights here are:
This is done after the page's model loads. This may or may not make sense depending on your context.
Some people like to wrap each model in its own controller, but I didn't have clean mappings to controllers like that, so I directly set these property values.
It's generally bad practice to set computed properties that are promises, so if you have to deal with promises (which with any use of this.store.find() you do, then it's best to resolve the promise in the route and then pass the "concrete" property to your controller. But keep in mind that your template will be rendering these values when they eventually resolve! So, again there is some room for debate.
I think the general takeaway is that Ember is giving you lots of options to get this done, with plenty of possibilities to use depending on your needs.
I have a set of exchange rates that are being used to calculate prices on order objects, and I'm trying to figure out the best way to make these available whereever they're needed (Models, basically just flat Ember.Objects, and Controllers).
I've searched a lot for a good solution, and there's a lot of old code examples that no longer seem to work, as well as a lot of talk about dependency injection, but no good examples on how to use it.
So I've resorted to this, which feels kinda dirty, but this way I can call window.App.exchangeRates whereever I need it:
var ApplicationController = Ember.Controller.extend({
init: function() {
this._super();
var exchangeRates = new ExchangeRates();
exchangeRates.refresh();
this.set('exchangeRates', exchangeRates);
// Expose the exchangeRates to the global scope.
window.App.exchangeRates = exchangeRates;
},
});
More specificly, I need it injected into my Model, which is created by the Router like this:
var BuyBankTransferIndexRoute = Ember.Route.extend({
setupController: function (controller) {
controller.set('model', BuyOrder.create({
type: 'bank_transfer',
}));
}
});
And then bound inside the model, like this:
var BuyOrder = Ember.Object.extend({
init: function () {
// Whenever the currency data reloads.
window.App.exchangeRates.addObserver('dataLoaded', function () {
// Trigger a re-calculation of the btcPrice field.
this.propertyWillChange('currency');
this.propertyDidChange('currency');
}.bind(this));
},
});
Is there a better way? I'm using Ember App Kit with Ember 1.2.0 here.
I would say your best bet is to use the 'needs' functionality of controllers. You have a exchangeRates property on your application controller. If you wanted to, say, have that variable in a posts controller, you could do this:
App.PostsController = Ember.Controller.extend({
needs: ['application'],
exchangeRates: Ember.computed.alias('controllers.application.exchangeRates')
});
The router of my application looks like this (it's CoffeeScript):
App.Router.map () ->
#resource 'conversations', { path: '/' } ->
#resource 'conversation', { path: ':conversation_id' }
#route 'new'
So, in my app, I have paths like /new, /1, /2, etc.
I would like to detect a transition from /1 to /2 to make some initializations in my view (basically, put the focus on a textarea field). Unfortunately, as /1 and /2 use the same route, it seems nearly impossible to detect that transition.
I tried by using didInsertElement in the view (as described here) or by observing currentPath in the controller (as described here). It works fine if I go from /new to /1 (different routes) but not if I go from /1 to /2.
I found this gist suggesting to use the StateManager but it seems outdated (and I'm not sure it's really what I need).
What do you suggest me to do?
EDIT
It seems that setupController is called every time so I decided to overload it like this:
App.ConversationRoute = Ember.Route.extend {
setupController: (controller, model) ->
controller.set 'model', model
# do something here?
}
And I want the init method in my view to be called:
App.ConversationView = Ember.View.extend {
init: ->
#$('form textarea').focus()
}
But I still can't figure out how to make these two things work together (it's a problem because I read that the controller is not supposed to know about the view).
Thank you for your help!
Use the didInsertElement view hook and an observer.
App.ConversationView = Ember.View.extend
didInsertElement: ->
#focusOnTextarea()
modelChanged: (->
#focusOnTextarea()
).observes('controller.model')
focusOnTextarea: ->
#$('form textarea').focus()
In the case of going from /1 to /2, the route and view are not changing. Ember does the least amount of work possible. There's no need to re-render the view, so it doesn't. But this tripped me up too, and I think it's a big gotcha.
Also, if you override init in your view, make sure to call #_super().
Note: the model hook is only called when landing on a page to deserialize the URL, not when transitioning from another page and changing the model instance.
Route#model is your friend here. It will receive a params hash containing information from the URL on every route change (even when changing just which instance of a class is being viewed) In your case,
App.ConversationRoute = Ember.Route.extend {
model: (params) ->
App.Conversation.find params.conversation_id
setupController: (controller, conversation) ->
// you have the correct Conversation object
The guides have more examples.
The didInsertElement method of the view is the best method if you need to instantiate something on your view. If you need to have the controller do something when the template loads, you can put the call in the setupController method of your route:
App.FirstRoute = Ember.Route.extend({
setupController: function(controller){
controller.onload();
}
});
Here's a jsfiddle with a full example:
http://jsfiddle.net/jgillick/Q5Kbz/
This will be called each time that route is loaded. Try the jsfidle. Click along to the second template and then use your browser's back button. The onload should fire again.
Also, fun fact, you can use the deactivate method as an unload, to do anything you need to that controller when the user navigates away from that route:
App.FirstRoute = Ember.Route.extend({
deactivate: function(){
this.controller.unload();
}
});
Unrelated
One thing to note, (not directly related to your question) if you set the model on the controller in the setupController method, it will overwrite your controllers content property each time that route is loaded. To prevent this, put a conditional around the assignment:
setupController: function(controller, model) {
controller.onload();
if (model && (!controller.content || Ember.keys(controller.content).length === 0)) {
controller.set('content', model);
}
}