Bind to the number of objects - ember.js

I'm trying to understand how I would create a binding or computed property that is the number of objects I have. I can get the number (I think) via:
App.MyObject.all().get("length")
When I create a controller property with that inside a function it doesn't update as more objects are downloaded.
numOfMyObjects: function(){
return App.MyObject.all().get("length");
}.property()
Right now I have this in my ApplicationController, but it just shows 0.
I'd like to know how to do this for all of the objects and then also for a filtered set of objects.
Thanks.

You need to tell Ember on which properties it should observe to fire the numOfMyObjects method. For example:
numOfMyObjects: function(){
return App.MyObject.all().get("length");
}.property('myArray.length');
However, this won't work in your case because you've got App.MyObject in your controller itself, instead you want to be instructing the appropriate route which model(s) the controller should represent.
This way you won't actually need to create a computed property, because you'll have access to the model in your Handlebars.
Please see the JSFiddle I've put together as an example: http://jsfiddle.net/ESkkb/
The main part of the code lies in the IndexRoute:
App.IndexRoute = Ember.Route.extend({
model: function() {
return App.Cat.find();
}
});
We're telling the IndexController that it should represent all of the cats. And then once we've done that, we can display the cats in our view, or in our case, the number of cats:
Count: {{length}}

Related

Ember computed property for retrieving one record out of a hasMany relationship?

Here's my situation, simplified:
// model/price-source.js
export default DS.Model.extend({
price: DS.attr('number'),
product: DS.belongsTo('product')
)};
// model/product.js
export default DS.Model.extend({
priceSources: DS.hasMany('price-source')
)};
In my products template, I want to be able to simply refer to the source with the lowest price, like so:
// templates/products.hbs
{{#each model as |product|}}
<span>{{product.cheapestSource.price}} €</span>
{{/each}}
How would I go about setting up the cheapestSource computed property? I imagine I'd have to do something like this:
// model/product.js
cheapestSource: Ember.computed('priceSources', function() {
let sources = this.get('priceSources');
let cheapest = sources.get('firstObject');
// iterate over sources and set cheapest to whichever has the lowest price
return cheapest;
})
The problem is, I have little idea how to loop through the hasMany relationship (apart from using the handlebars {{#each}} helper), and whether a computed property can even consist of a single Ember Data record from another model. Does sources.#each somehow play into this, if so, how?
Any help and ideas are appreciated, thanks.
I got it working by sorting the priceSources into a computed property sortedPrices, then calling the firstObject of the sortedPrices in the template. Will edit this post with the actual solution soon.
It took ages to test because I didn't realize that commenting out handlebars blocks will break the rendering of html inside them. Note to self...
EDIT: This did it:
export default DS.Model.extend({
priceSources: DS.hasMany('price-source'),
sortProperties: ['price:asc'],
sortedSources: Ember.computed.sort('priceSources', 'sortProperties')
});
Then in the template:
<span>{{product.sortedSources.firstObject.price}} €</span>
Works ok, without a ton of code.
This you can do on a controller where you need to use cheapestSource.
cheapestSource: Ember.computed('priceSources', function() {
let sources = this.get('priceSources');
let cheapest = sources.get('firstObject');
let array = sources.mapBy("price");
let min = array.reduce(function(a, b, i, array) {return Math.min(a,b)});
sources.forEach(function(source){
if (source.get("price") == min){
cheapest = source;
}
});
return cheapest;
})
Model is bit hard to achieve what you want this is one why using one computed and after template is render computed becomes object that you need.
cheapestSource: Ember.computed('priceSources', function() {
let product = this;
this.get('priceSources').then((sources)=>{
let array = sources.mapBy("price");
if(array.length>0){
let min = array.reduce(function(a, b, i, array) {return Math.min(a,b)});
sources.forEach(function(source){
if (source.get("price") == min){
product.set("cheapestSource", source);
}
});
}
});
})
When I have issues like this I use active model adapter on Rails and return for example cheapestSourcePrice as part of product in my custom serializer then in Ember product model just add cheapestSourcePrice and in template {{product.cheapestSourcePrice}} You dont want ember to do heavy lifting like this but if you dont control the server then do it like this. And one more thing after it sets source to cheapesetSource computed is no more until refresh. If you need it to stay computed you must add one more property on model then set him insted example
cheapestSource2: DS.attr()
this will allow for it to be an object
product.set("cheapestSource2", source);
and then in template
{{product.cheapestSource}}{{product.cheapestSource2.price}}
first property you call is there so computed is called.
If you got time try this solution too. this.get('priceSources') it returns Promise so you need to access resultant in then method and wrap it in DS.PromiseObject so that you can access it like normal object in template.
cheapestSource: Ember.computed('priceSources.#each.price', function() {
return DS.PromiseObject.create({
promise: this.get('priceSources').then(sources => {
let resultObj = {}
//sources is normal array you can apply your logic and set resultObj
return resultObj;
})
});
})

When creating a new data object in Ember that relies on another object how do you pass it along?

I am on a page where I can see a specific customer, part of my router.js is:
this.route('customers');
this.route('customer', {path: "customers/:customer_id"});
this.route('customer.order.create', { path: "customers/:customer_id/order/create" });
customer.order.create needs to load in my main view and so is not nested. An order 'has a' customer.
I've setup my /customer/order/create controller to have
needs: "customer"
I want to access the customer in my /customer/order/create.hbs template like this:
<h3>New Order for {{controllers.customer.name}}</h3>
When I end up creating the order I will also want to set newOrder.customer = customer.
customer.hbs links like so
<div>
{{#link-to 'customer.order.create' model}}Create Order{{/link-to}}
</div>
Currently {{controllers.customer.name}} renders nothing, what piece of the puzzle am I missing to get to the customer in my order/create route?
Or putting it more generally, what route/controller/etc code do I need when I have a parent object which belongs to my child object in a /parentObject/parent_id/childObject/create type scenario.
There are many points to fix:
1) {{controllers.customer}} is Controller Object, {{controllers.customer.name}} it's name property. I think you want {{controllers.customer.model.name}}.
2) "..newOrder.customer = customer.." should be
newOrder.set('customer', this.get('controllers.customer.model'));
3) your customer.order.create route model hook shoudn't be empty, since you are using dynamic segment customer_id:
//route
model: function(params) {
return this.find('customer', params.customer_id);
}
4) Your routes are not nested, so {{controllers.customer.model.name}} would be empty if your customer route is not activated. So you should use: {{model.name}} instead of {{controllers.customer.model.name}}
When you click link you passes model directly, and model hook is not fired, so all looks good. When you visit url directly, model hook is fired. If it is empty you will see nothing.
General concept: it is dependancy injection concept, you could read here: http://guides.emberjs.com/v1.12.0/understanding-ember/dependency-injection-and-service-lookup/
You should be able to get the customer from the store. Give the following code a try:
The route:
export default Ember.Route.extend({
model: function(params) {
return Ember.RSVP.hash({
customer: this.store.find('customer', params.customer_id)
});
}
});
The controller:
export default Ember.Controller.extend({
customer: Ember.computed.alias('model.customer')
});
And it should be directly accessible as customer in your template, like so:
<h3>New order for {{customer.name}}</h3>
I changed needs: "customer" to needs: ["customer"] and then used {{model.name}} in my template. Seems that needs requires an array of strings and not just a string, after I fixed that Ember took compare of the rest without the need to create a /customers/order/create.js route.
For a more complete answer see Artych's answer if you don't want everything taken care of.

Unbound properties as arguments for components and views

I'm in the process of optimizing my Ember application by following some of the tips given in this presentation. I'm wondering how I can give unbound properties as arguments to components and views. For instance in
{{my-component arg=unboundProperty}}
I want unboundProperty to be unbound, i.e. it takes as value its first non-null value (set after the models in the route has been resolved) but does not propagate to the component when its value changes. How can I achieve that?
If you really need to do it you can use a computed property without defining dependencies. The computed property will be calculated the first time it's requested, and then it will never think it needs to update, so it will never update.
App.FooController = Ember.ObjectController.extend({
realProperty: 'fooBar',
unboundProperty: function(){
return this.get('realProperty');
}.property()
});
{{my-component arg=unboundProperty}}
You could do the same thing in your component
App.MyComponentComponent = Ember.Component.extend({
readOnceArg: function(){
return this.get('arg');
}.property()
})

In Ember.js how to notify an ArrayController's corresponding itemController, when a property on the ArrayController changes

I have an EmailsController (ArrayController), which stores all the emails. I have an EmailController (ObjectController) that has a parameter that stores if the actual Email is selected or not. I am trying to implement a button in the emails template, that selects or deselects all the Emails. So somehow I need to notify the EmailController via an action of the EmailsController and change the EmailController's isChecked parameter.
I am trying to use the itemController, the needs, and the controllerBinding parameters, but nothing works.
Here are the controllers:
App.EmailsController = Ember.ArrayController.extend({
needs: ["Email"],
itemController: 'Email',
checkAll: true,
actions: {
checkAllEmails: function() {
this.toggleProperty("checkAll");
console.log(this.get("checkAll"));
}
}
});
App.EmailController = Ember.ObjectController.extend({
needs: ["Emails"],
controllerBinding: 'controllers.Emails',
isChecked: true,
checkAllChanged: function() {
//this should execute, but currently it does not
this.set("isChecked",this.get('controller.checkAll'));
}.property("controller")
});
Here is the corresponding jsFiddle: http://jsfiddle.net/JqZK2/4/
The goal would be to toggle the selection of the checkboxes via the Check All button.
Thanks!
Your mixing a few different mechanisms and your using a few wrong conventions. It's not always easy to find this stuff though, so don't fret.
Referencing Controllers
Even though controllers are created with an Uppercase format, the are stored in the lowercase format and your needs property should be:
needs: ['emails'],
You then access other controllers through the controllers property:
this.get('controllers.emails.checkAll')
Computed Properties
Computed properties can be used as a getter/setter for a variable and also as a way to alias other properties. For example, if you wanted the isChecked property on the Email controller to be directly linked to the value of the checkAll property of the Emails controller, you could do this:
isChecked: function() {
return this.get('controllers.emails.checkAll');
}.property('controllers.emails.checkAll')
Although computed properties can do much more, this basic form is really just a computed alias, and there is a utility function to make it easier:
isChecked: Ember.computed.alias('controllers.emails.checkAll')
Observables
An observable basically creates a method that will be called when the value it observes changes. A computed alias would cause all items to uncheck or check whenever you clicked on any one of them, since their isChecked property is linked directly to the checkAll property of the parent controller. Instead of your checkAllChanged method identifying as a property it should use observes:
checkAllChanged: function() {
this.set("isChecked",this.get('controllers.emails.checkAll'));
}.observes("controllers.emails.checkAll")
This way when the checkAll property changes on the parent controller, this method updates the isChecked properties of all items to its value, but if you uncheck or check an individual item, it doesn't affect the other items.
Bindings
Bindings are somewhat deprecated; from reading issues on the Ember github repository I believe the creators of Ember seem to favor using computed properties, aliases, and observables instead. That is not to say they don't work and if your goal was to avoid having to type out controllers.emails every time, you could create one like you did (I wouldn't call it controller though, cause thats really quite ambiguous):
emailsBinding: 'controllers.emails'
Using a computed alias instead:
emails: Ember.computed.alias('controllers.emails')
You could then change your observer to:
checkAllChanged: function() {
this.set("isChecked",this.get('emails.checkAll'));
}.observes("emails.checkAll")
Heres an updated version of your jsFiddle: http://jsfiddle.net/tMuQn/
You could just iterate through the emails, changing their properties from the parent controller. You don't need to specify needs or observe a variable.
App.EmailsController = Ember.ArrayController.extend({
itemController: 'email',
actions: {
checkAllEmails: function() {
this.forEach(function(email) {
email.toggleProperty("isChecked");
});
}
}
});
Also, you typically don't set initial values like you did with isChecked = true; I believe that's creating a static shared property on the prototype (not what you intended). Instead, set the property on init, or pass it in from your original json data.
See the code: http://jsfiddle.net/JqZK2/5/

ObjectController and ArrayController

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.