I'm building ember app, and I have date selector at the top and a few tabs. Each tab represents a model to work with, but all models need date selector. So I store the date selector values as applicationController properties, and I've reached the point where I need to load data with store.query("Model", {date: applicationController.date}) and now I'm lost. If I use ModelController with hooks like:
export default Ember.Controller.extend({
appController: Ember.inject.controller('application'),
myNeedData: function() {
this.store.findAll('myNeedData',
{date: this.get('appController').get('selectedUrlDate')}
);
}.property('appController.selectedUrlDate')
})
everything actually works, but it is a hack. So I need to load model data through Route's model(). But how can I pass applicationController property to Route and make it observe the changes?
Thanks, Kitler for pointing out the path of researching. So I've made the service
export default Ember.Service.extend({
store: Ember.inject.service(),
loadModel(date) {
// some important actions
}
});
then the controller functions:
export default Ember.Controller.extend({
nextMonth() {
var date = this.get('selectedDate');
date.setMonth(date.getMonth() + 1);
this.set('selectedDate', new Date(date));
},
prevMonth() {
var date = this.get('selectedDate');
date.setMonth(date.getMonth() - 1);
this.set('selectedDate', new Date(date));
},
});
and the route:
export default Ember.Route.extend({
modelService: Ember.inject.service('my-service'),
model() {
return this.prepareModel();
},
setupController: function(controller, model) {
controller.set('model', model);
},
prepareModel() {
const date = this.controllerFor('application').get('selectedUrlDate');
return this.get('modelService').loadModel(date);
},
actions: {
nextMonth() {
const self = this;
self.controllerFor('application').nextMonth();
self.refresh();
},
prevMonth() {
const self = this;
self.controllerFor('application').prevMonth();
self.refresh();
},
}
});
So now I have data manipulation in route, not important repeatable property currentDate and its manipulation in application controller, and route accordingly changes model on user interactions due to refresh!
Related
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.
In our app, there is a Company page, which lists all the companies in paginated way. We are using Ember 1.13 and ember-cli-pagination addon for pagination. The data is coming from Rails API, so we are using remote paginated API scenario. Everything is working fine for now, 10 records on each page, next and previous buttons etc. Only problem is when we add a new record, the record is saved but it doesn't show up on the UI immediately, we have to refresh the page for that. There are other parts of our app where we don't have pagination, so when we add a new record, UI is updated immediately without any refresh.
One issue has been reported in the repo of the addon related to this - https://github.com/mharris717/ember-cli-pagination/issues/89
I tried that but it didn't work. Any suggestions?
EDIT
models/company.js
import DS from 'ember-data';
import Ember from 'ember';
import EmberValidations from 'ember-validations';
import Notable from './notable';
export default Notable.extend(EmberValidations, {
// Attributes
companyName : DS.attr('string'),
companyNotes : DS.attr('string'),
.
.
.
// Associations
owner : DS.belongsTo('user', { inverse: 'companiesOwned' }),
assignedTo : DS.hasMany('user', { inverse: 'companiesAssigned', async: true }),
.
.
.
avatar : DS.belongsTo('attachment', { async: true }),
persons : DS.hasMany('person', { async: true }),
.
.
.
});
routes/company.js
import Ember from 'ember';
import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin';
import RouteMixin from 'ember-cli-pagination/remote/route-mixin';
export default Ember.Route.extend(RouteMixin, AuthenticatedRouteMixin, {
model: function(params) {
return Ember.RSVP.hash({
company : this.findPaged('company', params),
users : this.store.findAll('user'),
.
.
.
});
},
setupController: function(controller, models) {
this._super(controller, models);
controller.set('model', models.company);
controller.set('users', models.users);
.
.
.
}
});
controllers/company.js
import Ember from 'ember';
import pagedArray from 'ember-cli-pagination/computed/paged-array';
export default Ember.Controller.extend({
listView : false,
newCompany : false,
creatingCompany : false,
showingCompany : false,
editingCompany : false,
// Pagination
queryParams: ['page', 'perPage'],
pageBinding: 'content.page',
perPageBinding: 'content.perPage',
totalPagesBinding: 'content.totalPages',
page: 1,
perPage: 10,
disableDelete : true,
actions: {
createCompany: function() {
// debugger;
var company = this.get('store').createRecord('company');
this.set('company', company);
this.set('editCompanyPane', true);
this.set('disableDelete', true);
},
// Edit Company
editCompany: function(company) {
this.set('company', company);
this.set('editCompanyPane', true);
this.set('disableDelete', false);
},
closeEditCompany: function() {
this.get('company').rollback();
this.set('editCompanyPane', false);
this.set('disableDelete', true);
},
saveCompany: function(company) {
var _this = this;
company.save().then(function() {
Ember.get(_this, 'flashMessages').success('company.flash.companySaveSucessful');
_this.set('editCompanyPane', false);
}, function() {
Ember.get(_this, 'flashMessages').danger('apiFailure');
});
},
// Delete Company
deleteCompany: function(company) {
var _this = this;
company.destroyRecord().then(function() {
Ember.get(_this, 'flashMessages').success('company.flash.companyDeleteSucessful');
_this.set('editCompanyPane', false);
}, function() {
Ember.get(_this, 'flashMessages').danger('apiFailure');
});
},
}
})
Templates is just a list of ten records per page. Exactly same as Remote pagination API example.
{{#each model as |company|}}
<div class="col-md-3">
{{partial 'companies/modal-view'}}
</div>
{{/each}}
{{ page-numbers content=content }}
Put an observer on your model's content length in your controller:
modelLengthObs: function () {
alert('Model's count has changed');
}.observes('model.content.length')
Is it firing? If it isn't it means you created a record but didn't add it to your controller's model. You can do this manually by doing this in your controller after the creation of the record:
this.get('model.content').pushObject('newRecord');
Your model is associated with a list of companies. Adding a new company like you do isn't gonna refresh your model (you aren't reloading the route), so it isn't gonna refresh your template. Try manually adding the new company to your controller's model's content.
Is it working?
PS : You can try pushing it directly to your model instead of its content.
I had the same problem, this line worked for me:
this.get('target.router').refresh();
I added it to the controller of that hbs:
export default Ember.Controller.extend(
{
actions:
{
deleteSomething: function(someData)
{ //do this
$.get("/deleteSomething.php?some_data=" + someData );
//and refresh
this.get('target.router').refresh();
}
}
});
you can find a reference with code example here.
I have an Ember Data model as:
export default DS.Model.extend({
name: DS.attr('string'),
friends: DS.hasMany('friend',{async:true}),
requestedFriendIds: Ember.computed('friends',function(){
return this.get('friends').then(function(friends){
return friends.filterBy('status','Requested').mapBy('id');
});
})
});
I have a route setup that uses it:
export default Ember.Route.extend({
model: function(params){
return Ember.RSVP.hash({
memberProfile:this.store.find('member-profile', params.memberprofile_id).then(function(memberProfile)
{
return Ember.RSVP.hash({
requestedFriendIds:memberProfile.get('requestedFriendIds'),
UserId:memberProfile.get('user.id'),
Id:memberProfile.get('id')
});
}),
});
}
});
},
And htmlbars that utilize the route model. My computed property is always correctly called on a reload, but isn't refreshed on a user action 'Add Friend', which changes the store by adding a friend and the profile.friends' record like this:
actions:
{
addFriend:function(profile_id,)
{
this.store.findRecord('member-profile',memberProfile).then(function(member){
var friend = this.store.createRecord('friend',
{
member:member,
status:'Requested',
timestamp: new Date(),
});
friend.save();
member.get('friends').pushObject(friend);
member.save();
}.bind(this));
}
}
Some notes: I've tried the computed property on 'friends','friends.[]'. My code base is Ember 2.0.1, with Ember.Data 1.13.12, and as such 'friends.#each' is deprecated. The underlying data is correctly updated in the backing store (EmberFire). I've debugged into EmberData and I see that the property changed notifications invalidation code is called. This is only a selection of the code...
What am I missing...? Is there a better way to approach this?
I think you should watch friends.[] instead of only friends:
requestedFriendIds: Ember.computed('friends.[]',function(){
return this.get('friends').then(function(friends){
return friends.filterBy('status','Requested').mapBy('id');
});
})
And you could probably put your action in your route and manually refresh model (it might be issue with promise result not binding to changes in CP). So, in your route:
actions: {
addFriend(profile_id) {
this.store.findRecord('member-profile', memberProfile).then(member => {
let friend = this.store.createRecord('friend',
{
member:member,
status:'Requested',
timestamp: new Date()
});
friend.save();
member.get('friends').pushObject(friend);
member.save();
this.refresh();
});
}
}
The most important part is using this.refresh() in Ember.Route.
I am using Ember 1.13.9 an Ember-data 1.13.11 and struggling to have Ember Data do what I would like. As an example, I have a model called "goal" and a
goals: Ember.on('init', Ember.computed(function() {
const {store} = this.getProperties('store');
return store.findAll('goal');
})),
When this runs it does query the database and put the appropriate records into the store BUT getting them out of the store is my problem. I would have thought that once the Promise resolved that I'd be able to iterate over the array of results. Using the inspector I can see that at clients.goals.content.content (where clients is the name of the server I see this from the inspector:
First of all this is pretty deep into the structure. I was hoping Ember's "get" would allow me to simply say something like data.get('content.0.id') but this just comes back as undefined. Second of all the crazy structure continues in that each of these listed objects are InternalModel objects which only have the following structure to them:
Note that:
there are two InternalModels, that is the right number (matches store results)
the id property is available here
there is an internal property called _data which has the other attributes of the record
Ok so in a completely hacky way I could pull out what I need but surely I shouldn't be writing code like:
_goals: Ember.on('init', function() {
const {store} = this.getProperties('store');
store.findAll('goal').then(data => {
let result = [];
data.forEach(item => {
let record = item.get('data'); // this gets what's in _data apparently
record.id = item.get('id');
result.push(record);
}
this.set('goals', result);
}),
Yuck. What am I missing?
If you need to convert Ember model to plain object you can use Model.serialize or Model.toJSON methods.
Update:
If you need to not just extract the data from models but to access fetched models via computed property, there are several ways to implement it.
1) Synchronous property (collection):
Controller:
import Ember from 'ember'
export default Ember.Controller.extend({
goals: [],
someProperty: Ember.computed('goals.#each', function () {
var goals = this.get('goals');
goals.forEach(goal => {
console.log( goal.get('someProperty') );
});
})
});
Route:
import Ember from 'ember'
export default Ember.Route.extend({
setupController: function (controller, model) {
this._super(controller, model);
this.store.findAll('goal').then(goals => {
controller.set('goals', goals);
});
}
});
Template:
{{#each goals as |goal|}}
{{log goal}}
{{/each}}
2) Asynchronous property (promise):
Controller:
import Ember from 'ember'
export default Ember.Controller.extend({
goals: Ember.computed(function () {
var storeGoals = this.store.peekAll('goal') || [];
if (storeGoals.length) {
return RSVP.resolve(storeGoals);
} else {
return this.store.findAll('goal')
}
}),
someProperty: Ember.computed('goals.#each', function () {
var goals = this.get('goals').then(resolvedGoals => {
resolvedGoals.forEach(goal => {
console.log( goal.get('someProperty') );
});
});
})
});
Template:
{{#each goals as |goal|}}
{{log goal}}
{{/each}}
I have a certain route that shows a list of projects, and it gets initial data from my RESTAdapter based on who the user is.
I am now implementing a search function that will issue a new API call so the user can get records besides the default ones for them, and the response should replace the model for that route. I have all that working, but I'm not sure how to do a loading or progress indicator (as the response from the database could potentially take 5-10 seconds depending on the amount of data). I know about loading substates, but in this case I'm not transitioning between routes. I just want to have at minimum a spinner so the user knows that it's working on something.
Would anyone that's done this before be willing to share how they handled a)replacing the model with new data, and b)keeping the user informed with a spinner or something?
Form action called when user clicks the Search button
searchProjects: function() {
var query = this.get('queryString');
if (query) {
var _this = this;
var projects = this.store.find('project', {q: query});
projects.then(function(){
_this.set('model', projects);
});
}
}
a) replacing the model with new data
You don't need to do anything. If you sideload records properly from the backend, Ember will automatically update them on the frontend.
b) keeping the user informed with a spinner or something
The loading substate is an eager transition. Ember also supports lazy transitions via the loading event.
You can use that event in order to display the spinner.
Here's an example from the docs:
App.ApplicationRoute = Ember.Route.extend({
actions: {
loading: function(transition, route) {
showSpinner();
this.router.one('didTransition', function() {
hideSpinner();
});
return true; // Bubble the loading event
}
}
});
UPD1
I need to do at least what I'm doing right? Setting the model to the response?
You need to reflect the search in the URL via query params. This will let the router automatically update the model for you.
what I would put in showSpinner to affect stuff on the page (like, can I use jQuery to show or hide a spinner element?), or show the actual loading substate.
I would set a property on that page's controller:
App.IndexRoute = Ember.Route.extend({
queryParams: {
search: {
refreshModel: true
}
},
model () {
return new Ember.RSVP.Promise( resolve => setTimeout(resolve, 1000));
},
actions: {
loading (transition, route) {
this.controller.set('showSpinner', true);
this.router.one('didTransition', () => {
this.controller.set('showSpinner', false);
});
return true;
}
}
});
App.IndexController = Ember.Controller.extend({
queryParams: ['search'],
search: null,
showSpinner: false,
});
Demo: http://emberjs.jsbin.com/poxika/2/edit?html,js,output
Or you could simply put the spinner into the loading template, which will hide obsolete data:
http://emberjs.jsbin.com/poxika/3/edit?html,js,output
Or you could put your spinner into the loading template:
Just in case others want to see, here's my working code based on #lolmaus's answers.
These Docs pages were helpful as well
Route's queryParams and Find method
Controller
//app/controllers/project.js
export default Ember.ArrayController.extend({
queryParams: ['q'],
q: null,
actions: {
searchProjects: function() {
var query = this.get('queryString');
if (query) {
this.set('q', query);
}
}
}
})
Route
export default Ember.Route.extend(AuthenticatedRouteMixin, {
model: function(params) {
if (params.q) {
return this.store.find('project', params);
} else {
return this.store.findAll('project');
}
},
queryParams: {
q: {
refreshModel: true
}
},
actions: {
loading: function(/*transition, route*/) {
var _this = this;
this.controllerFor('projects').set('showSearchSpinner', true);
this.router.one('didTransition', function() {
_this.controllerFor('projects').set('showSearchSpinner', false);
});
return true; // Bubble the loading event
}
}
});
My issue now is that when I use the parameter query, it works great, but then if I clear the query (with an action, to effectively "go back") then the records fetched by the query stay in the store, so when it does a findAll() I have both sets of records, which is not at all what I want. How do I clear out the store before doing findAll again?