EmberJS: How do I move stuff out of the application controller? - ember.js

I have found that nice example of how to display notifications within ember [1]. After noticing it does not work so well with the current ember versions, I created a jsbin and fixed what seemed to be broken: http://emberjs.jsbin.com/decojele/1/edit
I was thinking about putting something like that into its own module, so it can be easily added to existing ember applications. However, the first thing that does not seem to be very suitable for something like this is that it relies on methods and fields (like the notifications array) that are kept within the application controller:
App.ApplicationController = Ember.Controller.extend({
notifications: Em.A()
// ....
//please see jsbin for the other stuff
})
This is picked up by a Collection view, which handles the actual rendering.
App.NotificationContainerView = Ember.CollectionView.extend({
contentBinding: 'controller.notifications'
// ....
//please see jsbin for the other stuff
});
As far as I currently understand how controllers work is that they depend on the currently active route and I so far haven't been able to put what currently is inside the ApplicationController into something more specific like a NotificationController. Is there a good way to do this? Or is this probably something I am just overthinking?
[1] http://aaron.haurwitz.com/#!/posts/growllike-notifications-with-emberjs

As described here [1], if render is used instead of the view helper, the appropriate controller is called as well. So, that actually seems to work: http://emberjs.jsbin.com/decojele/2/edit?html,js,console,output
[1] http://darthdeus.github.io/blog/2013/02/10/render-control-partial-view/

Related

Ember: Re-render a route's template when the model changes

I've noticed that a route doesn't re-render when the model changes (i.e. transitionTo the same route with a different model). I have some jQuery plugins set up on a particular page and I need them to re-render when the model changes, so it appears as a fresh page.
Is there a way to do this? Perhaps by observing the model's ID and firing a re-render of the route somehow?
Thanks in advance
I have an ember twiddle that, I believe, does what you're looking for, but first I would like to argue there are no straightforward ways to do what you're asking because it is the opposite of what an SPA is designed to do.
Data binding (without refreshing the view) is typically a boon of an SPA, and the SPA works hard to avoid brute force reloading/refreshing/rerendering the view at all costs. It took me a while to find a solution to your question as it is stated because it seems to go against Ember design principles. Even hooks like route refresh() are meant to update the model and bound data, not reload the template.
Although other people have asked the same question before, it seems that most answers guide users towards not refreshing the whole view. More often than not, the ideal of refreshing the view is an incorrect assumption.
Following previous examples, I would suggest that your goal shouldn't be to refresh the template completely, but rather, figure out how you can make your jQuery plugin better fit in to a Single Page App/client-side JS friendly design and have it reload as a natural part of the route lifecycle.
For instance, maybe the plugin can be reloaded/reset/re-run in afterModel() or somewhere similar.
That said, I was able to accomplish what you asked for using (in my opinion, a hack) Ember.run.later() so that I could invalidate an if block and force the content to rerender. Note, this is typically not what users want since (aside from design principle reasons) it causes UI flicker.
I have a component like so.
/* will-rerender.hbs */
{{#if show}}
{{yield}}
{{/if}}
And it has a JS file like so.
/* will-rerender.js */
import Ember from 'ember';
export default Ember.Component.extend({
show: false,
didReceiveAttrs() {
this._super(...arguments);
/*
Ugly hack, but we need to reset `show` in
a separate run loop in order to force the view
to rerender.
*/
this.set('show', false);
Ember.run.later(() => {
this.set('show', true);
});
}
});
You can invoke it like this...
/* your template */
{{#will-rerender cacheKey=model.id}}
... your content to rerender ...
{{/will-rerender}}
Whenever the model.id changes the component will invoke didReceiveAttrs() causing show to invalidate and the view to refresh.
As an aside, I think the behavior of switching between models would be much more natural with {{link-to}} rather than calling transitionTo yourself.

emberjs providing data for multiple components

I have an Ember 2.11 application template with a few component placeholders at the moment (menu, breadcrumbs, related items) and an outlet which displays the main content which works fine. Now that I'm feeling more comfortable with the basics, I'm ready to try getting the breadcrumbs working.
I read about services, but I don't see that it is the right solution for breadcrumbs because it doesn't need to be persistent, it is based off the route. Although it is based off the route, I don't want to use the route literally as I want to use nice titles, and when viewing a specific item, the route doesn't accurately reflect what the breadcrumbs should show.
Since the breadcrumbs is based off the model that is being used for the display, I feel that I should be able to construct a breadcrumb object and then pass that into the component from the application template. I suppose this was the purpose of the controller back in the day. My thought was in the route to construct a breadcrumb object/property and return it with the model like RSVP and then I could access both in the template to pass the appropriate object to the appropriate component. But that seems wrong as the route should return an Ember data object, promise or Javascript array.
My current line of thinking is along these lines.
template/application.hbs
{{bread-crumbs crumbs=model.breadcrumbs}}
{{outlet}}
route/category/show
export default Ember.Route.extend({
model(params) {
let recipe = this.get('store').query('recipe', { category: params.category_id});
let crumbs = [{name: 'Category', link: 'category'},
{name: recipe.category.title, link: 'category.show', target: recipe.category.id}];
return {recipe: recipe, breadcrumbs: crumbs};
}
});
I'm not sure if this is the right way to approach this or if this will cause problems with async data fetching with Ember data. Something like this I would have to define on each route, but I don't have a lot of routes and seems to offer flexibility when I'm displaying a recipe (route is /recipe/recipe_id), but have the breadcrumbs show Home > Categories > Main Dishes > My Awesome Dish.
How would you approach the problem?
Updated 2017-02-10:
It appears that the model is not passed to the application template, only the route template. I'm not sure how to pass data 'down' to the application template.
You could probably create a breadcrumb like this by tracking the elements in a service, but I'd check out the ember-crumbly addon. It seems like it will meet your needs.
Remaining in your thinking line, if you want to pass your model as a variable of your controller in the route that you are accessing you need something like this:
export default Ember.Route.extend({
model(params){
let recipe = ...;
let crumbs = ...;
return {...};
},
setupController(controller, model){
this._super(controller, model);
controller.set('variable', model);
}
});
Having this, in your controller you can access to the crumbs like this:
this.get('variable');
And in then with an Ember.computed you can create a variable for display in your template.
Sorry for the minimize your code but I'm not in my computer.
Another approach is setting the crumbs variable at the init of the application controller, this set the variables defined in that controller global to the application, so you can modify them from other controllers and in the application controller lookup for that changes via didUpdateAttrs() or with didUpadteElement().
Hope this resolve your problem.
Greetings

filtering hasMany association in Ember

I'm brand new to Ember and stuck on something that seems very basic. So far in my e-commerce application, I have two models, Product, and Style; a Product has many Styles. For each product, I want to list a subset of the styles (e.g., those that are in stock or out of stock). Coming from a Rails background, I thought I would create a model method in Product called stockedStyles, which filters its styles and returns only those with stocked=true. That didn't work, so I tried another approach of using a controller method, but struck out there too.
Here's the code so far: http://jsbin.com/mufumowabi/1/edit?html,js,console,output
While I would definitely like to know the best practices way of doing this, I'm also curious about other approaches people can suggest that would work but are not recommended -- and why they aren't recommended. Especially when I'm learning something new, I like knowing all the different ways you could do something and then choosing the best/cleanest one.
If there's a tutorial that covers this sort of thing, please send it my way. I couldn't find anything that does this sort of thing, even though it seems so basic.
Lastly, I've found debugging Ember to be somewhat of a black box. For example, for the non-working code posted here, the JS console just says "error". Tips on how I would get more information about why what I'm doing is wrong would be most appreciated as well.
TIA,
fana
I feel your pain. I too came from a rails background expecting similarities in the implementation only to get confused initially. Nobody is ever exaggerating when they claim Ember requires a very large learning investment, but trust me if you stick around it's totally worth it.
Real quick let's take care of a simple goof: You can assign an object property to be either Ember.computed, or function() { /***/ }.property('sdf'); You can't have both. So make that computed function either:
unstockedStyles: Ember.computed.filterBy('styles', 'stocked', false);
or
unstockedStyles: function() {
return this.get('styles').filterBy('stocked', false);
}.property('styles.#each.stocked')
but you can't do both at once.
Ember Models vs Rails Models
Next, the difference with Ember, coming from rails perspective, is that models in Ember.js are intended to be extremely light, serving only as a minimal binding between the data source and the framework overall. Rails takes quite literally the opposite approach, encouraging a very heavy model object (and this is still a large source of contention in the rails community).
In ember.js, the model method helpers are intended to be placed in the controller objects (again, counterintuitive coming from rails). Moving that out, you'll want your models to look like this:
App.Product = DS.Model.extend({
title: DS.attr(),
styles: DS.hasMany('style', { async: true })
});
App.Style = DS.Model.extend({
desc: DS.attr(),
stocked: DS.attr("boolean")
});
The reason for this difference from Rails is that the role of controllers in Ember.js is for "decoration" of your object, whereas in Rails its to handle incoming/outgoing data logic. For each page, you may want to render the same data in different ways. Thus, the model will remain the same, and the controller takes on the burden of encapsulating the extra fluff/computation. You can think of decoration in the same way you were taught the inheritance pattern in OO, with a slight twist:
Let's say you want to have a Person base class (your Ember model), but then you extend it to Teacher and Student subclasses (your controllers) in order to add an additional propertiy that may be from the same type but is not modeled in the same way. For example, Teachers and Students have a many-to-many relationship to courses, but you want to model Students as attending their courses, where Teachers are instructing them. The controller acts as a way to provide such a filter.
ArrayController vs ObjectController
As for the controllers, computed properties for individual records should be placed in the ArrayController's associated ObjectController (we'll call it ProductController), so your controllers now look like this:
App.IndexController = Ember.ArrayController.extend();
App.ProductController = Ember.ObjectController.extend({
unstockedStyles: Ember.computed.filterBy('styles', 'stocked', true)
});
Finally, while Ember.js can automatically associate ObjectControllers with their associated ArrayController for resources defined in your router, you're loading a Product array on the IndexController, so you need to tell IndexController to use ProductController for its item behavior:
App.IndexController = Ember.ArrayController.extend({
itemController: 'product'
});
App.ProductController = Ember.ObjectController.extend({
unstockedStyles: Ember.computed.filterBy('styles', 'stocked', true)
});
Handlebars Binding
Not much here except for a small mistake: while you've enabled a computed property correctly in the proper controller, your {{#each}} loop is instead bound to the original styles array. Change it to use the new property:
{{#each unstockedStyles}}
<li>
{{desc}}, in stock: {{stocked}}
</li>
{{/each}}
And now you're good to go!
JSBin of fixes here: http://jsbin.com/sawujorogi/2/edit?html,js,output

Recommended way to rerender page in Ember after language switch

I need to rerender whole page application after language has switched. I don't like to use observers for it because of performance issues.
Language switch is done by a Ember.View. I tried to rerender parentView after changing Ember.I18n.translations but running into a known bug. It works one time but Ember Inspector shows parentView has dutzend of children views of it's own afterwards. After another switch parentView got destroyed. Just like demonstrated in this JSFiddle.
Here is simplified code of my view:
export default Ember.View.extend({
templateName: 'language-switch',
languageChanged: function() {
// change language
var language = this.get('controller.language.selected');
Ember.I18n.translations = translations[language];
// rerender page
this.get('parentView').rerender();
}.observes('controller.language.selected')
});
There is also a discussion about that problem on discuss.emberjs.com. I tried the suggestions there but they don't work. Last post suggest a work-a-round to map over all views, rerender them and afterwards use a transition. I don't like that one since I am afraid getting side problems by that hack.
I am pretty sure there must be a way to do a language switch in Ember with ember-i18n but what's the right way to do that?
Update:
I tried to implement the hack from discuss.emberjs.com. It's working but it's very limited. I implement it like this in the view:
var self = this;
var currentRoute = self.get('controller.currentRouteName');
this.get('controller.target').transitionTo('loading').then(function(){
self.get('controller.target').transitionTo(currentRoute);
});
Problem is that all data stored in model is lost. I did not find a way to get the current model and use it in transition. Also query parameters are lost. That limitation makes this work-a-round unacceptable for my application.
Update App.reset():
As suggested in comment I tried to use App.reset(). It works better than transitionTo work-a-round but doesn't match all needs. If calling App.reset() queryParams aren't lost but model is reseted. I'm looking for a way to rerender application while keeping current model and queryParams.
It seems that mostly these problem is handled by a full page reload on locale change. E.q. one of the main developers of ember-i18n said so in this discussion.

In an Ember view, what's the best way to run code after the rerendering of an internal child view?

I found a lot of questions about how to initialize jQuery plugins with dynamic data etc and usually the answer is that you should use the didInsertElement hook. My problem is that when using ember-data, initially the view is inserted empty and the template vars are filled afterwards through the bindings. Thus in didInsertElement the elements I need are not there.
I found a solution that works for me but I think there must be a better solution.
In addition to the didInsertElement method I also observe the attributes in the controller and call my code from there, too, wrapped in a Ember.run.next so the child view is rendered before.
Is there a better solution to react to updated child views?
It depends a bit on the size of your application, and the number of elements your view is rendering.
Edit: You might successfully observer the isLoaded property of the RecordArray. Some details here: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/record_arrays/adapter_populated_record_array.js#L21
One option that I have used for views that have a single object as their content is something like:
MyApp.CustomView = Ember.View.extend({
content: null,
contentObserver: function() {
this.rerender();
}.observes('content')
}
If your view contents is a list, it makes little sense to re render the view for each item in the list, though.
However, I think this approach is fairly similar to what you are already doing, though.
Another approach is to implement your own adapter with Ember Data. This way, you can tell the rest of your application that it's finished loading your data. Something like:
MyApp.Adapter = DS.Adapter.create({
//Finding all object of a certain type. Fetching from the server
findAll: function(store, type, ids) {
var url = type.url;
jQuery.getJSON(url, function(data) {
store.loadMany(type, data);
});
//Perform some custom logic here...
}
}
This should work, but its not as generic as it should/could be though.
Also, you might want to take a look at this commit, which allows for registering a one-time event.
https://github.com/emberjs/ember.js/commit/1809e65012b93c0a530bfcb95eec22d972069745#L0R19
I had this problem and came up with a solution: make a handlebars helper that tells you when the sub-section has rendered. Hope that helps some other folks!