Cannot pass queryParams - ember.js

I'm newbie in ember.js and i'm trying to sort a list of products in my app.
I have a route catalog/category.js:
import Ember from 'ember';
export default Ember.Route.extend({
queryParams: {
ordering: {
refreshModel: false,
},
},
model (params) {
return Ember.RSVP.hash({
categories: this.store.peekAll('category'),
category: this.store.peekRecord('category', params.category_id),
});
}
});
and controller catalog/category.js:
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: ['ordering'],
ordering: null,
sortedProducts: Ember.computed.sort('model.category.products', 'ordering'),
});
Link passes a parameter:
{{#link-to 'catalog.category' (query-params ordering='price')}}Price Asc{{/link-to}}
ordering parameter is setting to price and it doesn't work. But when I manually set the ordering parameter to ['price'] - everything works as expected.
Can anyone suggest how to fix it?

ordering parameter is setting to price and it doesn't work. But when I
manually set the ordering parameter to ['price'] - everything works as
expected.
There is a difference between string:price (set by query param) and array:['price'] (set by you manually).
You need another property which will create that array for you:
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: ['ordering'],
ordering: null,
orderingObserver: Ember.on('init', Ember.observer('ordering', function() {
let ordering = this.get('ordering');
let array = [];
if (ordering) {
array.push(ordering);
}
this.set('orderingArray', array);
})),
sortedProducts: Ember.computed.sort('model.category.products', 'orderingArray'),
});
isn't it supposed to work "out of the box"?
It works with arrays of properties - http://emberjs.com/api/classes/Ember.computed.html#method_sort.

Related

Recommended pattern to implement JSONAPI filter with query params in Ember?

I spent a chunk of time yesterday trying to include filter (reflecting the JSONAPI spec) in the query params of part of an Ember app. With Ember Data it is easy enough to pass a filter array to an endpoint, the problem I have is reflecting that filter array in the query params for a particular route. Note: other, non array, query params are working fine.
TL;DR I have tried various options without success and have a solution that really feels unsatisfactory and not at all DRY. I figure that many others must have tackled this problem and have surely found a better solution. Read on for details of what I have tried so far.
I started with something like this (I initially assumed it would work having read the Ember docs on query params):
Controller:
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort', 'filter'],
sort: 'id',
init() {
this._super(...arguments);
this.set('filter', []);
},
});
Route:
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
filter: {
refreshModel: true
},
sort: {
refreshModel: true
}
},
model(params) {
console.log(JSON.stringify(params)); // filter is always []
return this.get('store').query('contact', params);
}
});
Acceptance Test (this was just a proof of concept test before I started on the more complex stuff):
test('visiting /contacts with query params', async function(assert) {
assert.expect(1);
let done = assert.async();
server.createList('contact', 10);
server.get('/contacts', (schema, request) => {
let params = request.queryParams;
assert.deepEqual(
params,
{
sort: '-id',
"filter[firstname]": 'wibble'
},
'Query parameters are passed in as expected'
);
done();
return schema.contacts.all();
});
await visit('/contacts?filter[firstname]=wibble&sort=-id');
});
No matter how I tweaked the above code, params.filter was always [] in the Route model function.
I have searched around for best-practice on what would seem to be a common use case, but have not found anything recent. sarus' solution here from Nov 2015 works, but means that every possible filter key has to be hardcoded in the controller and route, which seems far from ideal to me. Just imagine doing that for 20 possible filter keys! Using sarus' solution, here is code that works for the above acceptance test but as I say imagine having to hardcode 20+ potential filter keys:
Controller:
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort',
{ firstnameFilter: 'filter[firstname]' }
],
sort: 'id',
firstnameFilter: null,
init() {
this._super(...arguments);
}
});
Route:
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
firstnameFilter: {
refreshModel: true
},
sort: {
refreshModel: true
}
},
model(params) {
if (params.firstnameFilter) {
params.filter = {};
params.filter['firstname'] = params.firstnameFilter;
delete params.firstnameFilter;
}
return this.get('store').query('contact', params);
}
});
I hope there's a better way!
If you don't have the requirement to support dynamic filter fields, #jelhan has provided a really good answer to this question already.
If you do need to support dynamic filter fields read on.
First of all, credit should go to #jelhan who put me on the right track by mentioning the possibility of serializing the application URL with JSON.stringify() and encodeURIComponent() together.
Here's example code with this working...
Controller:
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort', {
filter: {
type: 'array'
}
}],
sort: 'id',
init() {
this._super(...arguments);
this.set('filter', []);
},
});
Route (no changes required):
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
filter: {
refreshModel: true
},
sort: {
refreshModel: true
}
},
model(params) {
return this.get('store').query('contact', params);
}
});
Acceptance Test:
test('visiting /contacts with query params', async function(assert) {
assert.expect(1);
let done = assert.async();
server.createList('contact', 10);
server.get('/contacts', (schema, request) => {
let params = request.queryParams;
assert.deepEqual(
params,
{
sort: '-id',
"filter[firstname]": 'wibble',
"filter[lastname]": 'wobble'
},
'Query parameters are passed in as expected'
);
done();
return schema.contacts.all();
});
// The filter is represented by a Javascript object
let filter = {"firstname":"wibble", "lastname":"wobble"};
// The object is converted to a JSON string and then URI encoded and added to the application URL
await visit('/contacts?sort=-id&filter=' + encodeURIComponent(JSON.stringify(filter)));
});
Great! This test passes. The filter defined in the application URL is passed through to the Route. The Route's model hook makes a JSONAPI request with the filter correctly defined. Yay!
As you can see, there's nothing clever there. All we need to do is set the filter in the correct format in the application URL and the standard Ember Query Params setup will just work with dynamic filter fields.
But how can I update the filter query param via an action or link and see that reflected in the application URL and also make the correct JSONAPI request via the Route model hook. Turns out that's easy too:
Example Action (in controller):
changeFilter() {
let filter = {
firstname: 'Robert',
lastname: 'Jones',
category: 'gnome'
};
// Simply update the query param `filter`.
// Note: although filter is defined as an array, it needs to be set
// as a Javascript object to work
// this.set('filter', filter); - this seems to work but I guess I should use transitionToRoute
this.transitionToRoute('contacts', {queryParams: {filter: filter}});
}
For a link (say you want to apply a special filter), you'll need a controller property to hold the filter, we'll call it otherFilter and can then reference that in the link-to:
Example Controller property (defined in init):
init() {
this._super(...arguments);
this.set('filter', []);
this.set('otherFilter', {occupation:'Baker', category: 'elf'});
}
Example link-to:
{{#link-to 'contacts' (query-params filter=otherFilter)}}Change the filters{{/link-to}}
There you have it!
There is no reason to represent filter values in applications URL the same way as they must be for backend call to be JSON API complaint. Therefore I would not use that format for application URLs.
If you don't have the requirement to support dynamic filter fields, I would hard code all of them to have nice URLs like /contacts?firstname=wibble&sort=-id.
Your code would look like this, if you like to support filtering for firstname and lastname:
// Controller
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['sort', 'page', 'firstname', 'lastname'],
sort: 'id',
});
// Route
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
firstname: {
refreshModel: true
},
lastname: {
refreshModel: true
}
sort: {
refreshModel: true
}
},
model({ firstname, lastname, sort, page }) {
console.log(JSON.stringify(params)); // filter is always []
return this.get('store').query('contact', {
filter: {
firstname,
lastname
},
sort,
page
});
}
});
If you have to support dynamic filter fields, I would represent the filter object in application URL. For serialization you could use JSON.stringify() and encodeURIComponent() together. The URL would then look like /contacts?filter=%7B%22firstname%22%3A%22wibble%22%7D&sort=-id.

Computed property on a service that accesses the store

I wrote a service for loading notifications:
import Ember from 'ember';
export default Ember.Service.extend({
sessionUser: Ember.inject.service(),
store: Ember.inject.service(),
read() {
let currentUserId = this.get('sessionUser.user.id');
return this.get('store').query('notification', {
userId: currentUserId,
read: true
});
},
unread() {
let currentUserId = this.get('sessionUser.user.id');
return this.get('store').query('notification', {
userId: currentUserId,
read: false
});
}
});
I want to change the colour of an icon in the navigation bar when there are unread notifications. The navigation bar is a component:
import Ember from 'ember';
export default Ember.Component.extend({
notifications: Ember.inject.service(),
session: Ember.inject.service(),
hasUnreadNotifications: Ember.computed('notifications', function() {
return this.get('notifications').unread().then((unread) => {
return unread.get('length') > 0;
});
})
});
And the template then uses the hasUnreadNotifications property to decide if the highlight class should be used:
<span class="icon">
<i class="fa fa-bell {{if hasUnreadNotifications 'has-notifications'}}"></i>
</span>
However, it doesn't work. Although the store is called and notifications are returned, the hadUnreadNotifications doesn't resolve to a boolean. I think this is because it returns a promise and the template can't deal with that, but I'm not sure.
Questions
Is it idiosyncratic ember to wrap the store in a service like this. I'm doing this because it feels clumsy to load the notifications in the application route just to show the count.
Why doesn't hasUnreadNotifications return a boolean?
Is it possible to make read and unread properties instead of functions, so a computed property can be created in the service to calculate the count?
Returning promise from computed property will not work. Computed properties are not Promise aware. to make it work you need to return DS.PrmoiseObject or DS.PromiseArray.
You can read other options available from this igniter article.
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Component.extend({
notifications: Ember.inject.service(),
session: Ember.inject.service(),
hasUnreadNotifications: Ember.computed('notifications', function() {
return DS.PromiseObject.create({
promise: this.get('notifications').unread().then((unread) => {
return unread.get('length') > 0;
})
});
})
});

Computer Property not updating on hasMany

I have a pretty simple model:
//models/build
import DS from 'ember-data';
export default DS.Model.extend({
shipments: DS.hasMany('shipment')
});
I need to update a computer property whenever a new shipment is added or removed, so I'm using my controller, like this:
//controllers/build
import Ember from 'ember';
export default Ember.Controller.extend({
init: function() {
this.get('allShips');
this.get('ships');
},
allShips: Ember.computed.alias('model.shipments'),
ships: function() {
console.log('updating ships');
}.property('model.#each.shipments')
});
I'm not consuming the computer properties in my template anywhere, I just need to keep them updated to do some computational work so I'm just getting them in the init function. I'm getting "updating ships" in the console when I'm entering the build route correctly, but after that it won't update no matter how many shipments I add or remove.
I've tried many different properties, model.ships.#each, allShips, #each.allShips, allShips.#each, and dozens of all combinations, but all were in vain. I've never had this kind of trouble with computer properties so any advice would be appreciated.
The placement of #each is incorrect - it should be after model.shipments.
As one of the comments state - if you're not accessing properties (e.g. model.shipments.#each.id then you should observe models.shipments.[].
For some reason the computed property only updates for me when returning an array - not returning a value or returning any type of value other than an array fails.
Model
//models/build
import DS from 'ember-data';
export default DS.Model.extend({
shipments: DS.hasMany('shipment')
});
Controller
//controllers/build
import Ember from 'ember';
export default Ember.Controller.extend({
init: function() {
this.get('allShips');
this.get('ships');
},
allShips: Ember.computed.alias('model.shipments'),
ships: function() {
var ships = this.get('model.shipments');
console.log('updating ships');
return [];
}.property('model.shipments.#each')
});
It's probably best to observe model.shipments.[] if you're not returning a value:
//controllers/build
import Ember from 'ember';
export default Ember.Controller.extend({
doShipsWork: function() {
console.log('updating ships');
}.observes('model.shipments.[]')
});

Expose controller property to route's model function

I've defined some properties in a controller for a pagination:
import Ember from 'ember';
export default Ember.ArrayController.extend({
limit: 1,
skip: 0,
pageSize: 1
}
});
I'd like to access limit in the Route's model-function but I don't know how.
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
console.log(this.get('controller').get('limit')) <- doesnt work for example
return this.store.find('post', {limit: 1,
sort:'createdAt desc'});
}
});
Maybe you should take a look at the queryParams option (http://emberjs.com/guides/routing/query-params/).
With query params you can set the limit to be a query param in your URL like http://yourdomain.com/someroute?limit=15.
Your controller will become:
export default Ember.ArrayController.extend({
queryParams: ['limit'], // Here you define your query params
limit: 1 // The default value to use for the query param
});
You route will become:
export default Ember.Route.extend({
model: function(params) {
return this.store.find('post', {
limit: params.limit, // 'limit' param is available in params
sort:'createdAt desc'
});
}
});
Alternative:
If you don't want to use query params, another solution might be to define the limit property in one of the parent route's controller. By doing so you can access the property in the model hook by doing:
this.controllerFor('parentRoute').get('limit');

Accessing ArrayController elements using needs

I've got the following 2 controllers:
controllers/student/index.js
import Ember from 'ember';
export default Ember.ObjectController.extend({
hasDebt: function(){
var totalCredit = this.get('totalCredit');
var totalCreditSpent = this.get('totalCreditSpent');
if (totalCreditSpent > totalCredit)
{
return true;
}
return false;
}.property('payments.#each', 'lessons.#each'),
});
controllers/students.js
import Ember from 'ember';
export default Ember.ArrayController.extend({
itemController: 'student/index',
sortProperties: ['fullName'],
sortAscending: true,
debts: function(){
var allDebts = [];
var totalDebts = 0;
this.forEach(function(student){
if (student.get('hasDebt'))
{
allDebts.push({
name: student.get('fullName'),
amount: student.get('availableCredit')
});
totalDebts += student.get('availableCredit');
}
});
return {'all': allDebts, 'total': totalDebts};
}.property('this.#each.payments', 'this.#each.lessons'),
});
And everything is working as expected. I'm able to access the hasDebt property of each element through the itemController.
Now I'd like to show the debts in a dashboard in the IndexRoute, so I've created the following additional controller, hoping to be able to access the StudentsController by using needs:
controllers/index.js
import Ember from 'ember';
export default Ember.Controller.extend({
needs: ['students'],
debts: function(){
var debts = [];
console.log( this.get('controllers.students.debts') );
this.get('controllers.students').forEach(function(student){
console.log('student');
});
return debts;
}.property(''),
});
I seem unable to access the StudentsController and any of its properties.
What am I doing wrong?
I believe that a computed property must observe a property in order to ever be populated. In your example:
controllers/index.js
export default Ember.Controller.extend({
needs: ['students'],
students: Em.computed.alias('controllers.students'),
debts: function() {
...
}.property('students.debts')
});
In this example I also made it a little easier to use Students by providing a Computed Alias mapped to students in the controller.
Debugging
It's also very handy to use the browser's console when debugging. Try running something like the following and see what comes back.
App.__container__.lookup('controller:index').get('students')
This assumes your application exists under the App namespace.