Let's say I have a template which iterates over a collection of items, and I want to call a function with each item which is specific to the controller, and not a model-level concern:
{{#each people as |person|}}
icon name: {{findIconFor(person)}}
{{/each}}
I'd like to define findIconFor in the controller, because this is something specific to this particular view.
export default Ember.Controller.extend({
findIconFor: function(person) {
// figure out which icon to use
}
);
But that doesn't work. The template fails to compile. Parse error: Expecting 'STRING', 'NUMBER', 'ID', 'DATA', got 'INVALID'
What is the "ember way" to do this?
As i spent almost entire day on a similar problem here is my solution.
Because Ember for some reason just doesn't allow you to run a controller functions directly from the template (which is ridiculous and ties your hands in some very stupid ways and i don't know who on earth decided this is a good idea ...) the thing that makes most sense to me is to create an universal custom helper, that allows you to run functions from the template :) The catch here is that you should always pass the current scope (the "this" variable) to that helper.
So the helper could be something like this:
export default Ember.Helper.helper(function([scope, fn]) {
let args = arguments[0].slice(2);
let res = fn.apply(scope, args);
return res;
});
Then, you can make a function inside your controller, that you want to run, for example:
testFn: function(element){
return element.get('name');
}
and then in your template you just call it with the custom helper:
{{#each items as |element|}}
{{{custom-helper this testFn element}}}
{{/each}}
The first two arguments to the helper should always be "this" and the name of the function, that you want to run, and then you can pass as many extra arguments as you wish.
Edit: Anyway, every time when you think you need to do this, you should think if it will not be better to create a new component instead (it will be in 90% of the cases)
I'd use a computed property in the controller:
iconPeople: Ember.computed('people.#each', function(){
var that = this;
return this.get('people').map(function(person){
return {
'person': person,
'icon': that.findIconFor(person)
};
});
})
Now you could get the icon from {{person.icon}} and the name from {{person.person.name}}. You might want to improve on that (and the code is untested), but that's the general idea.
If the icon is something associated with a person, then since the person is represented by a model, it is best to implement it as a computed property on the person model. What is your intent in trying to put it into the controller?
// person.js
export default DS.Model.extend({
icon: function() { return "person-icon-" + this.get('name'); }.property('name')
..
};
Then assuming that people is an array of person:
{{#each people as |person|}}
icon name: {{person.icon}}
{{/each}}
The alternative provided by #jnfingerle works (I assume you figured out that he is proposing that you loop over iconPeople), but it seems like a lot of extra work to go to to create a new array containing objects. Does the icon depend on anything known only to the controller? If not, as I said, why should the logic to compute it be in the controller?
Where to put things is a a matter of philosophy and preference. Some people like bare-bones models that contain nothing more than fields coming down from the server; other people compute state and intermediate results in the model. Some people puts lots of stuff in controllers, whereas others prefer light-weight controllers with more logic in "services". Personally, I'm on the side of heavier models, lighter controllers, and services. I'm not claiming that business logic, or heavy data transformations, or view preparations should go in the model, of course. But remember, the model represents an object. If there's some interesting facet to the object, whether it come down from the server or be computed somehow, to me it makes a lot of sense to put that in the model.
Remember also that controllers are part of a tightly-coupled route/controller/view nexus. If there's some model-specific thing that you compute in one controller, you might have to then add it to some other controller that happens to be handling the same model. Then before you know it you're writing controller mixins that share logic across controllers that shouldn't have been in them in the first place.
Anyway, you say your icon comes from an "unrelated data store". That sounds asynchronous. To me, that hints that maybe it's a sub-model called PersonIcon which is a belongsTo in the person model. You can make that work with the right mix of adapters and serializers for that model. The nice thing about that approach is that all the asynchronicity in retrieving the icon is going to be handled semi-magically, either when the person model is created, or when you actually need the icon (if you say async: true).
But perhaps you're not using Ember Data, or don't want to go to all that trouble. In that case, you could consider adorning the person with the icon in the route's model hook, making use of Ember's ability to handle asynchronous model resolution, by doing something like the following:
model: function() {
return this.store.find('person') .
then(function(people) {
return Ember.RSVP.Promise.all(people.map(getIcon)) .
then(function(icons) {
people.forEach(function(person, i) {
person.set('icon') = icons[i];
});
return people;
})
;
})
;
}
where getIcon is something like
function getIcon(person) {
return new Ember.RSVP.Promise(function(resolve, reject) {
$.ajax('http://icon-maker.com?' + person.get('name'), resolve);
});
}
Or, if it is cleaner, you could break the icon stuff out into an afterModel hook:
model: function() { return this.store.find('person'); },
afterModel: function(model) {
return Ember.RSVP.Promise.all(model.map(getIcon)) .
then(function(icons) {
model.forEach(function(person, i) {
person.set('icon') = icons[i];
});
})
;
}
Now Ember will wait for the entire promise to resolve, including getting the people and their icons and sticking the icons on the people, before proceeding.
HTH.
Related
I have a little problem using findAll in my component. It concerns the value it returns at the end.
users: Ember.computed(function() {
return this.get('store').findAll('user');
}),
In my case, I want to get the name of the first object. So in my handlebar:
users.firstObject.name
'users' is a class in this case. But I'm trying to return directly the first object in the property, like this:
user: Ember.computed(function() {
return this.get('store').findAll('user')
.then(function(user){
return user.get('firstObject');
});
}),
But in this case, in my handlebar, user.name is undefined and user is a promise. There is something I can't understand with promises, how they work ...
Can somebody help me to get the correct user without using 'firstObject' on my users ?
Thanks in advance !
The shortest way to solve your problem is to install an ember-promise-helpers addon and apply it in your template as follows:
{{#if (await user)}}
{{get (await user) 'name'}}
{{/if}}
However, AFAIK, it is not recommended to use promises as values for computed properties, although you can still do it. here I would recommend the documentation for Ember.PromiseProxyMixin as well as reading some (although older) forum threads (for instance this one).
I think the problem you're having is not with the promises, but how you're using the computed property.
The best place for a findRecord/findAll is in a model hook in the route. It is best to not rely on firstObject for what you're describing. You have no guarantees about which record will be first as your app's back end data changes.
Computed properties are really meant for watching other data and updating the displayed information about that data, not for fetching things from the store on their own. They require arguments that are the names of the data they are supposed to be watching for updates. Since your computed property isn't watching anything, it never fires. Here's an example.
firstName: null,
lastName: null,
fullName: Ember.computed('firstName', 'lastName', function() {
let firstName = this.get('firstName');
let lastName = this.get('lastName');
return `${firstName} ${lastName}`;
})
You can read more under Computed Properties in the Guides.
You will have a much easier time if you define a model hook in a route that gets the user's info using findRecord and then passes it to child components or sets it on a service if many different components on different routes need the user information.
I'm using this.store.push to push a record into the store from with the application controller (this action is being called from a socket service that is initialized in the application controller), using ember 2.2.1 I am achieving this like
var newStoreRecord = this.store.push({
data: {
id: id,
type: 'cart',
attributes: newCartItem
}
});
This adds this new item into the store but the template doesn't update to show the new item, I also tried adding something like this
this.get('cart.model').pushObject(newStoreRecord); assuming that I had something like cart: Ember.inject.controller(), at the top of the controller, might have had that one wrong anyway.
In the cart route I have my model being defined as so
model(params) {
this.set('routeParams',params.event_url);
return Ember.RSVP.hash({
event: null,
items: null
});
},
actions: {
didTransition() {
this.store.findRecord('event',this.get('routeParams')).then((result)=>{
this.controller.set('model.event',result);
});
this.controller.set('noItems',false);
this.store.query('cart',{auction_id:this.get('routeParams'),user:this.get('user.user.user_id'),combine:true}).then((result)=>{
if(!result.get('length')){
this.controller.set('noItems',true);
return null;
}else{
this.controller.set('model.items',result);
}
});
},
}
Not sure if I'm having troubles with getting the template to update because I'm not use the model hook? (btw, we're not using the model hook because of the bad performance on android we'd rather load an empty template with a loader and THEN load data rather than the other way around.
I have several thoughts here:
To answer your question specifically, when you set a variable from the store, like you're doing, it will only reference what was in the store at that time. It will not update automatically.
Your best bet is to add two new computed properties to your controller:
items: Ember.computed(function() {
return this.store.peekAll('cart');
}),
// You'll need to flesh this one out further
filteredItems: Ember.computed('items.#each.auction_id', function() {
return this.get('items').filter(...);
})
Reference filteredItems in your template and it should work.
Sidenote, I'd highly recommend refactoring a couple things.
I would use the setupController hook instead of didTransition. It runs after the model hook is complete so will be similar to what you're looking for
You can access the params at any time in the route, so you don't need to save them in the model hook
You don't need to return an a promise in the model hook if you're not doing any async data. Just return the object. You may need even need to do that.
Hope this helps.
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.
When rendering a template through a Backbone view you will often end up with some code that looks something like this:
ShirtView = {
template: JST["/templates/shirt_template"],
el: ".shirt-element"
render: function() {
var html = this.template({color: this.model.color, size: this.model.size});
this.$el.html(html);
}
}
This is all well and good and your template will render with the attributes you wanted. But if this.model.color changes then it will not be reflected in the view. You can then use something like modelbinder to explicitly bind elements in the view to your model, but this means introducing extra code to your view.
What I am wondering is if there are any templating engines, like Moustache or Handlebars, that automatically updates the elements belonging to the fields in the attributes object as the model changes, without me having to specify it in the view?
As the comments have suggested, there are several libraries you can use for this ... but I'd like to suggest that you don't need to. I work on a Backbone-powered site with thousands (heck, probably tens or hundreds of thousands) of lines of code, and all we use is our own custom base class.
In essence, all you need to do is:
var TemplatedView = Backbone.View.extend({
render: function() {
this.renderTemplate();
}.
renderTemplate: function() {
this.$el.html(this.template(this.model.toJSON()));
}
});
then you can make any new view a templated one with just:
var ShirtView = TemplatedView.extend({
template: JST["/templates/shirt_template"],
el: ".shirt-element"
});
or, if you have some custom render logic, you just need to call renderTemplate:
var AdvancedShirtView = TemplatedView.extend({
template: JST["/templates/shirt_template"],
el: ".shirt-element",
render: function() {
this.model.fold();
this.renderTemplate();
this.model.press();
}
});
Now we have a few enhancements beyond that (eg. if someone specifies a "rawTemplate" property on one of our views, our renderTemplate will compile it in to a proper template), but that's the beauty of rolling your own solution for something like this: you get exactly what you want.
Or you can use a library :-) But personally, for something that's both so simple and so integral to your site, I don't see why you'd want to.
I am learning emberjs form trek.github.com. That tutorial used both Em.ObjectController and Em.ArrayController. And There is also Em.Controller.
I am confused when to use them, I guess Em.ObjectController is for single object, Em.ArrayController is for array and Em.Controller is just for ApplicationController.
Is there any blessed rule for when to use which?
Usually, if your Controller represent a list of items, you would use the Ember.ArrayController, and if the controller represents a single item, you would use the Ember.ObjectController. Something like the following:
MyApp.ContactsController = Ember.ArrayController.extend({
content: [],
selectedContact: null
});
MyApp.SelectedContactController = Ember.ObjectController.extend({
contentBinding: 'contactsController.selectedContact',
contactsController: null
});
Then in your Ember.Router (if you use them), you would connect the two inside the connectOutlets function:
connectOutlets: function(router) {
router.get('selectedContactController').connectControllers('contacts');
}
Edit: I have never used the Ember.Controller. Looking at the source code, it seems like you might want to use this if you are building a custom controller that doesn't fit in with the two other controllers.
The general rule is that it depends on model from route.
If model is an array then you should use ArrayController. It will allow you to implement in easy way sorting or filtering in future. ArrayController is connecting usually ObjectControllers.
When your model is an instance of Ember Object then you should use ObjectController. It takes place when you are using for instance ember data. With Objectcontroller you can access model properties directly. You don't have to write model.property each time.
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return Ember.Object.create({name: 'Mathew'});
}
});
My name is {{name}}
Finally, when one doesn't have model there is an ideal situation to use just Ember.Controller. It is not going to allow direct access to model properties as ObjectController.