When trying to access the model of a controller when creating a computed property on the controller, I get the following error:
model.uniqBy is not a function
app/controller/ticket.js
export default Ember.Controller.extend({
statuses: Ember.computed('model', function() {
var model = this.get('model');
return model
.uniqBy('status')
.map(function(i) { return i.status; })
.toArray();
}),
});
The model I'm giving to the controller is a collection returned from this.store.findAll('ticket');, but trying to iterate through it seems to be causing the above error. Is the collection given to the model not supposed to be an Ember.Enumerable object? Should I be trying to access the collection via the DS.Store (in which case I don't understand the need to pass a model to the controller)?
Ember.computed.uniqBy
A computed property which returns a new array with all the unique elements from an array, with uniqueness determined by specific key
Please try this instead for your computed property
statuses: Ember.computed.uniqBy('model', 'status')
EDIT
You can use ember computed map on this property to fine tune your array if needed, for example like this
status: Ember.computed.map('statuses', function(status, index)
return status.toUpperCase() + '!';
})
Another way is that computed property uses dynamic aggregate syntax as described here
https://guides.emberjs.com/v2.6.0/object-model/computed-properties-and-aggregate-data/
so Ember.computed('model.#each.status', function()
Hope it helps
Related
I currently have component that displays a table of results based on the model passed into it:
\\pagedContent is a computed property of the model
{{table-test model=pagedContent}}
The table updates its contents as various filters are selected via query params. I've bee trying to implement some 'sort on click' behaviour to the table headings with the following code:
import Component from '#ember/component';
import {
computed
} from '#ember/object';
export default Component.extend({
model: null,
init() {
this._super(...arguments);
this.dataSorting = ['total_users']
this.dataSortingDesc = ['total_users:desc']
},
sortedDataDesc: computed.sort('unsortedData', 'dataSortingDesc'),
sortedData: computed.sort('unsortedData', 'dataSorting'),
unsortedData: computed('model', function () {
return this.get('model');
}),
actions: {
columnsort(property) {
if (!this.get('tableSorted')) {
this.set('dataSortingDesc', [`${property}:desc`])
this.set('model', this.get('sortedDataDesc'))
this.set('tableSorted', true)
} else {
this.set('dataSorting', [property])
this.set('displayModel', this.get('sortedData'))
this.set('model', null)
}
},
}
});
The sorting works as expected but I have a problem due to the two way binding of the model. Other components on the template also uses the model and when the data in the table is sorted, it creates all kinds of problems with those components.
I tried to create a seperate 'copy' of the model using a computed property like follows:
\\a new property
displayModel: computed('model', function () {
return this.get('model');
}),
sortedDataDesc: computed.sort('unsortedData', 'dataSortingDesc'),
sortedData: computed.sort('unsortedData', 'dataSorting'),
unsortedData: computed('model', function () {
return this.get('model');
}),
actions: {
columnsort(property) {
if (!this.get('tableSorted')) {
this.set('dataSortingDesc', [`${property}:desc`])
this.set('model', this.get('sortedDataDesc'))
this.set('tableSorted', true)
} else {
this.set('dataSorting', [property])
this.set('displayModel', this.get('sortedData'))
this.set('model', null)
}
},
The table then iterates over displayModel to create itself. This produces a behaviour where the columns sort but then the display 'freezes' once a column heading is clicked and does not update as the underlying model updates. In this case, I can see from my other components that the model continues to update as new filters are applied.
I was also unsuccessful using a oneWay and didUpdateAttrs implementation.
How can I create a copy of the model in the component so that I can sort the table columns without changing the whole model via two-way binding whilst keeping a one way bind so that if the model is updated by the parent template, it will also update in the component?
Edit:
I've created a twiddle here
If you click on the header of the table, you can see that both of the components change their order because I am working on the passed 'model'.
What I am trying to achieve is a workflow in which I can pass the model into the table component so it displays data and I can sort the columns without affecting the second component (also being fed by the model).
The problem is I also need the property populating the table to refresh if something else (a set of filters existing on my parent template) refresh the model through interaction on the parent template.
So a 'sort' affects the property populating the table and nothing else BUT the property populating the table is sensitive to model updates on the parent hosting the component.
The problem here is that you're sharing the array backing the models between components, and then manipulating the array (which Ember is aware of). If you stopped sharing the array (by copying the references into a second array):
import Ember from 'ember';
export default Ember.Route.extend({
model(){
return [{name: "Frank", age: 22}, {name: "Alan", age: 43}, {name: "Bob", age: 56}]
},
setupController(controller, model){
controller.set('model', model);
controller.set('tableModel', model.slice(0));
}
});
And change you're application.hbs like:
{{my-component model=tableModel}}
{{second-component model=model}}
you would only see the change in order happen to the table component. Since both arrays point to the same references, your models themselves are bound to both arrays (ie changing model properties like age affects both model and tableModel since they're actually pointing to the same piece of memory. But the sorting will only affect the tableModel since you've now allocated two arrays
I've expanded upon your gist with my own copy in which I manipulate a referenced model in the models array and it affected both models and tableModels since the underlying elements in the array are the same references.
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;
})
});
})
I have a controller where I get the value from the hbs, which sends me the selected country value. I need this selected country in the model to compute and return back some results back to the hbs. How set this value in controller and get it in the model so I can compute using that value?
Well, there may be some different approaches to achieve this. However, I will give you some example which will hopefully help you.
//Controller.js
notes: Ember.computed('model.notes.[]', 'model.notes.#each.date', function() {
return this.get('model.notes').sortBy('date').reverse(); //This is an example of Computed function which in this case it's sorting notes based on date.
}),
blink: null,
actions: {
taskChangeColor: function() {
this.set('blink', 'blinker'); // this is another example that set new data by action which can be retrive from model and set to property
}
}
or another thing that you can do is to use Computed function in Model itself like
// model.js which is using ember-data and moment
timeZone: DS.attr(), //for example one property coming from server
utcOffsetFormat: Ember.computed(function() {
let time = moment.tz(this.get('timeZone')).format('hh:mm a');
return time;
// using a computed function to instantiate another value based on existing model property which means you can simpley use this property instead of direct one.
})
Additionally, you still are eligible to use action in Route.js instead of controller an example would be :
//route.js
actions: {
changeSave: function(step) {
var something = {
contact: this.currentModel,
};
this.currentModel.set('step', something.contact);
this.currentModel.save().then(d => {
// set your alert or whatever for success promise
return d;
}).catch(e => {
console.log(error(e.message));
return e;
});
},
in above example you can see that I have set an action to save notes in model which easily can set() to the model with exact same property name and if you do this you will get the result back immediately in your view.
hope it can help you. I recommend to read Ember-Docs
I would say, for your requirement you don't need controller properties for selectedCountryValue. You can keep this value in model itself.
In route,
setupController(model,transition){
this._super(...arguments); //this will set model property in controller.
Ember.set(model,'selectedCountryValue','US'); //you can set default value
}
and inside controller, you create computed property with dependent on model.selectedCountryValue. and compute some results
result:Ember.Computed('model.selectedCountryValue',function(){
//compute something return the result
}
In template, you can use {{model.selectedCountryValue}} directly.
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
I've created a computed property that relies on all records in the store.
I've tried making the property update on adding/removing records with .property('todos.#each.id'), .property('model.#each.id'), .property('#each.id'), .property('#each') and other combinations, no luck so far. :( When i create new records, existing recrods' property would not update.
Here's a fiddle: http://jsbin.com/UDoPajA/211/edit?output
The property is otherTodos on the Todo controller. This property is used by the <select> dropdown list on the page (via {{view Ember.Select}}).
You're out of scope of the collection. You'll need to get access to the todos controller in order to have a computed property based off of its model. needs will handle this use case. http://emberjs.com/guides/controllers/dependencies-between-controllers/
Additionally to make an easy to access alias to the todos controller's model we use computed.alias. http://emberjs.com/api/#method_computed_alias
Todos.TodoController = Ember.ObjectController.extend({
needs:['todos'],
todos: Ember.computed.alias('controllers.todos.model'),
....
foo: function(){
}.property('todos.#each.id')
});
PS note of caution, in your code you are creating multiple instances of Ember Data filter, filter collections are meant to be live collections that are long living and update as records are added/removed from the store. You might just want to grab the model from todos and filter over it instead of creating a new store filter (which then avoids the async code as well, not that that is an issue).
Here's an implementation that would avoid that (no point in using it as a setter, you are only getting from it):
otherTodos: function() {
var model = this.get('model'),
thisId = model.get('id');
var todos = this.get('todos').filter(function (todo) {
return todo.get('id') !== thisId;
});
var selectContent = todos.map( function(todo){
var selectContent = {
title: todo.get('title'),
id: todo.get('id')
};
return selectContent;
});
return selectContent;
}.property('todos.#each.id'),
Here's an updated jsbin of your code: http://jsbin.com/UDoPajA/216/edit