Unbound properties as arguments for components and views - ember.js

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()
})

Related

Create a computed bool using relationship field Ember

I'm trying figure out how my computed property identify if my relationship was set.
Project = Model.extend({
participantes: hasMany('author')
...
I need change my css based if has a author.
{{my-component project=project}}
//---------- my-component.js
export default Ember.Component.extend({
classNameBindings: ['hasParticipante'],
hasParticipante = Ember.computed('project.participantes', function(){
//the code I need gonna here
})});
This will probably not work since a relationship is always a PromiseArray or PromiseObject. Probably you can check on the content:
Ember.computed.bool('author.content')
Your component seems completely wrong:
You can't use = in Object creation.
Ember.get needs two parameters. The context and the property, like get(this, 'post').
You should not use arrow functions for computed properties since you have no access to the object then.
But you don't need that line at all. Just do classNameBindings: ['hasAuthor']

Ember binding with dynamic computed alias

I have a component that is provided each record ('data') of a model, along with 'meta' information that defines the attribute of the record to use, and renders it to a table. Within the component I'm trying to bind the underlying record attribute to each UI element {{tdVal}}:
tdVal : function(){
return Ember.computed.alias('data.' + this.get('meta').get('field'));
}.property()
Unfortunately this just renders [object object] in the UI. For comparison the following renders all of the items correctly, but obviously does not bind:
tdVal : function(){
return this.get('data').get(this.get('details').get('field'));
}.property()
Am I going about this in completely the wrong way? Any help would be very much appreciated.
UPDATE
To add clarity, if I bind to a literal key instead of an attribute key derived from the meta information I still have exactly the same problem, so I don't think it's an issue with using a derived key:
tdVal : function(){
return Ember.computed.alias('data.partner_id');
}.property()
UPDATE
If I set the binding against the component as an attribute rather than a function assigned to the attribute, then it works. Problem is I can't do this as the key for the alias needs to be derived and not a literal:
export default Ember.Component.extend({
tdVal : Ember.computed.alias('data.partner_id')
})
I found the solution to this. I think the computed alias was failing when returned from a function due to timing issues. Instead I added it to init()
export default Ember.Component.extend({
tagName : '',
init: function(){
this._super();
this.set('tdVal', Ember.computed.alias('data.' + this.get('details').get('field')));
}
});
This has done the trick, everything renders as it should and updates to the UI are reflected in the model and vice versa.
The second way looks right. It doesn't bind because you didn't provide the properties to bind to:
tdVal : function(key, value){
var path = 'data.' + this.get('details.field');
if (value)
this.set(path, value);
return this.get(path);
}.property('data', 'details.field')

How is Sorting achieved in an Ember model without using Array Controller?

Every google result is about an ArrayController sorting. Need a sorting mechanism without using ArrayController.
There is a model where there are sort params. Like say 'sortOrder' as one of the properties in the model (which will be from a back end).
Will be rendering this model using #each but this should do the iteration based on the sortOrder property and not the model's ID property.
In Ember 2.0 SortableMixin is deprecated and is on its way out too.
In the Controller (not the ArrayController) you may define a new computed property like SortedUsers1,2,3 below:
export default Ember.Controller.extend({
sortProps: ['lastName'],
sortedUsers1: Ember.computed.sort('model', 'sortProps'),
sortedUsers2: Ember.computed.sort('content', 'sortProps'),
sortedUsers3: Ember.computed('content', function(){
return this.get('content').sortBy('lastName');
})
});
The assumption above is that the model itself is an array of users with lastName as one of user properties. Dependency on 'model' and 'content' look equivalent to me. All three computed properties above produce the same sorted list.
Note that you cannot replace 'sortProps' argument with 'lastName' in sortedUsers1,2 - it won't work.
To change sorting order modify sortProps to
sortProps: ['lastName:desc']
Also if your template is in users/index folder then your controller must be there as well. The controller in users/ would not do, even if the route loading model is in users/.
In the template the usage is as expected:
<ul>
{{#each sortedUsers1 as |user|}}
<li>{{user.lastName}}</li>
{{/each}}
</ul>
Here is how I manually sort (using ember compare)
import Ember from "ember";
import { attr, Model } from "ember-cli-simple-store/model";
var compare = Ember.compare, get = Ember.get;
var Foo = Model.extend({
orderedThings: function() {
var things = this.get("things");
return things.toArray().sort(function(a, b) {
return compare(get(a, "something"), get(b, "something"));
});
}.property("things.#each.something")
});
You just need to include a SortableMixin to either controller or component and then specify the sortAscending and sortProperties property.
Em.Controller.extend(Em.SortableMixin, {
sortAscending: true,
sortProperties: ['val']
});
Here is a working demo.
In situations like that, I use Ember.ArrayProxy with a Ember.SortableMixin directly.
An ArrayProxy wraps any other object that implements Ember.Array
and/or Ember.MutableArray, forwarding all requests. This makes it very
useful for a number of binding use cases or other cases where being
able to swap out the underlying array is useful.
So for example, I may have a controller property as such:
sortedItems: function(){
var items = Ember.ArrayProxy.extend(Ember.SortableMixin).create({content: this.get('someCollection')});
items.set('sortProperties', ['propNameToSortOn']);
return items;
}.property()
Like so: JSBin

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/

Bind to the number of objects

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}}