In my application, the data in one model is "meta" in nature and defines the attributes in another model. Imagine retrieving a database, where the column definition data determines the attributes of each row.
I can reopen() the row model so long as I have not yet requested rows from the store. A subsequent call to store.findAll('row') retrieves data with all the attributes defined.
If however I have already called store.findAll('row') prior to Column.reopen(), then I cannot find a way cause the store to acknowledge the new attributes.
I have tried without success:
unloading all the rows and finding them again
calling peekAll/update on the rows already in the store
calling Ember.defineProperty() on the row model instead of reopen.
Both existing and subsequent new rows do not get the new attributes.
The solution that works for me is:
let store = this.get('store');
Ember.getOwner(store).unregister('model:item');
Item.reopen(newAttributes);
Ember.getOwner(store).register('model:item', Item);
let currentRoute = this.get('currentRoute');
if (currentRoute) {
currentRoute.refresh();
}
I register the route with the service that updates the schema. When the schema change is detected (which I debounce to avoid thrashing), I unregister the model, update it with reopen, re-register it and refresh the route. There may be a way to get the store to reflect these changes without refreshing the route, but I could not find it.
Related
I'm new to ember and exploring its capabilities by building a small module. I came across a scenario where I need to update the ember model content synchronously. The ember-data model contains an array of objects as contents.
I was hoping to perform a few tasks as follows
Perform an array content reorder - for the sake of simplicity we
can assume swapping the first and last item.
Append a record
without a network call
Delete a record without a network call.
Doing these should automatically sync the data bindings/computed props
My data model after a peekAll call contains 10 records(shown below) on which I need to perform the above operations.
My model is as shown below
export default Model.extend({
testId: attr('number'),
name: attr('string')
});
What is the right approach to update the content record? Could someone please suggest how to proceed?
This looks to me like the results of running something like let arr = await store.findAll('test-model'), is that correct? This is probably a PromiseArray and you can access the data as a Javascript Array by calling arr.slice() on it. This will let you do normal array operations, though performing a content re-order doesn't really make much sense in this scenario. I assume you were using it as an example.
For adding and removing records without a network call you can do that by going back to the store and this is what is covered in the docs, you don't need to act on this Object you're looking at.
Adding a new record:
let testModel = store.createRecord('test-model', {
name: 'Lorem ipsum'
});
testModel.save(); //until you do this no network data will be sent
Removing a record:
let testModel = store.peekRecord('testModel', 1); //to get a record with ID of 1
testModel.deleteRecord();
testModel.save(); //until you run save no network is sent
Once you've taken action like this on the store the data structure you posted above may be updated to contain the new data depending on how you accessed it originally. You can also re-fetch data from the store which will now know about your adding a deleting of models (even though you haven't saved it back to the server yet)
If you haven't saved yet and you re-do a peekRecord you'll need to filter out any deleted records from the results.
let undeletedModels = this.store.peekAll('test-model').filter(m => !m.isDeleted);
At the moment, when an article is added to the store, my view is not updated when I use store.query(), filtering server side, in my route but it's updated when I use store.findAll() with filtering client side.
With findAll, filtering client side
//route.js
model() {
return this.get('store').findAll('article');
}
//controller.js
articleSorted: computed.filterBy('model', 'isPublished', true),
and with query filtering server side
//route.js
model() {
return this.get('store').query('article', { q: 'isPublished' }),
}
The fact is that findAll is reloading and query is not.
I've found this but did not understand
https://github.com/emberjs/ember.js/issues/15256
thanks for the question. I'll try to answer it the best I can but it would seem like some more documentation should be added to the Ember Guides to explain this situation 🤔
Essentially this.store.findAll() and this.store.query() do two very different things. findAll() is designed to be a representation of all of the entities (articles in your case) so it makes sense that the result will automatically update as the store finds more articles it should care about. It does this because it doesn't return an array of articles, it returns a DS.RecordArray that will automatically update.
query() on the other hand is designed to ask the backend every time what it expects the result to be, and you are usually passing a number of parameters to the query() call that the backend is using to find or filter results. It would be impossible for the frontend to know exactly how the backend interprets these query parameters so it is not possible for it to "auto-update" when a new article is added that would satisfy the same query.
Does that make sense? Would you like me to go into any more detail?
When using store.query to fetch data from the server, the view can still be auto-updated with new client-created store data before it's saved to the server, by using a "live" record array for it.
While data from store.query isn't live, data from store.peekAll is, so you can query first but then leverage store.peekAll for display. You can query before setting your model to the peeked data, or keep your query as the model but use some other property of peeked data for display. The important part is to ensure the query is resolved before peeking at the store.
Example based on the current code in your question:
// route.js
beforeModel() {
// using return ensures this hook waits for the promise to resolve before moving on
return this.store.query('article', { q: 'isPublished' });
}
model() {
// Queried server data should now be available to peek at locally,
// so we can set the model to a live version of it. Don't append filterBy here,
// otherwise it will no longer be live.
return this.store.peekAll('article');
}
// controller.js
// seemingly redundant filter since using query, but needed if there are other records
// in the store that shouldn't be displayed, and is recomputed when
// a record is added or removed from the store-based model
articleSorted: filterBy('model', 'isPublished', true) // filterBy imported from '#ember/object/computed'
// template.hbs
{{#each articleSorted as |article|}}
{{!-- this displayed list should update as new records are added to the store --}}
{{article}}
{{/each}}
Note that after a new record is saved to the server, the query can be updated via its update method or via a route refresh. This will re-run the query and get the updated results from the server. If the query is the model, that would look like model.update(). If it was saved to someOtherProperty, then someOtherProperty.update(). In either case, route.refresh() could be used instead to re-run all route hooks.
Some specific comments/examples that I think are helpful:
https://github.com/emberjs/ember.js/issues/15256#issuecomment-302894768
https://github.com/emberjs/ember.js/issues/15256#issuecomment-302906077
https://github.com/pouchdb-community/ember-pouch/issues/232#issuecomment-428927114
I have a model that calls a query on my store. When something happens that causes the desired model to be a different query, I need to reload the data on the active route by firing the same query just with different param values.
To illustrate, when I come to my items route it fires a query that looks like this
return this.store.query('item',{auction_id: this.get('localStorage.user.user_active_auction'),user_id:this.get('localStorage.user.user_id')}).then((result)=>{
if(result){
return result;
} else {
this.get('notification').notify({message: "An error occurred trying to get items.",success:false});
this.transitionTo('index');
}
});
The localStorage pieces are from a service. When the .user property of the service changes, I need to reload this query with the new .user_active_auction and .user_id values so new data gets placed in the template. (and thusly replaced what was there)
Can't seem to find how to do this as the .refresh() method reloads the cached url it used before rather than creating a new one. Not sure if maybe there is an adapter/serializer method I can implement that will allow me to tell it not to use the cached one?
I have quite a complex page in my application with lots of different models being shown. I live-update several of these models through a /updates REST call. I pass a last_request_timestamp parameter and it returns the models that were created or modified since the last call.
I add the models to the store by using store.push(model, model_json). However, the templates are not updated after the models have been pushed. How can I ensure that there is a binding between the models in the store and the view?
Ok, I figured it out. The Ember.js FAQ says
Filters, on the other hand, perform a live search of all of the records in the store's cache. As soon as a new record is loaded into the store, the filter will check to see if the record matches, and if so, add it to the array of search results. If that array is displayed in a template, it will update automatically.
...
Keep in mind that records will not show up in a filter if the store doesn't know about them. You can ensure that a record is in the store by using the store's push() method.
So in the controller for the view that I want to live-update, I use filter() on the store to fetch the models.
App.PostController = Ember.ObjectController.extend({
comments: function() {
var postId = this.get('id');
return this.get('store').filter('comment', function(comment) {
return comment.get('post.id') == postId;
});
}.property('comments')
});
Now, whenever I push() a new Comment to store, it is automatically added to the appropriate post in the view.
You probably need to explicitly push them into a collection that is being represented on the page by using pushObject. store.push will return a live record, so you could do something like this.
var book_record = store.push('book', model_json);
this.get('controllers.books.content').pushObject(book_record);
That's assuming that the BooksController is a standard ArrayController.
Unfortunately it requires two steps (but simple steps). You need to add it to the store and then save the record for it to propagate changes to listeners via the adapter.
var pushedRecord = store.push(model, model_json);
pushedRecord.save();
Also if you have multiple records you can call pushMany instead of pushing each individually. You still have to save each.
store.pushMany(model, jsonArray).forEach function (pushedInstance) {
pushedInstance.save();
}
Ember Data's Adapter saves edited records in different groups of Ember.OrderedSets, namely: commitDetails.created, commitDetails.updated, and commitDetails.deleted.
model.save() from model controller's createRecord() will be placed in the commitDetails.created group. model.save() from model controller's acceptChanges will placed be in the commitDetails.updated group. But I can't find in code where the placement association happens.
I know that they are instantiated in Ember Transaction's commit function (which calls Adapter's commit, in turn calling Adapter's save). Throughout this process, I can't figure out where exactly the records are sorted according to the created/updated/deleted criteria.
I'm not quite clear what you're asking, but if you're looking for where records get added to their appropriate commitDetails set, I believe this is the line you're looking for, in the commitDetails property itself.
Here's the relevant code.
forEach(records, function(record) {
if(!get(record, 'isDirty')) return;
record.send('willCommit');
var adapter = store.adapterForType(record.constructor);
commitDetails.get(adapter)[get(record, 'dirtyType')].add(record);
});
Let's walk through it.
forEach(records, function(record) {
if(!get(record, 'isDirty')) return;
The above says, for each record in the transaction, if it's not dirty, ignore it.
record.send('willCommit');
Otherwise, update its state to inFlight.
var adapter = store.adapterForType(record.constructor);
Get the record's adapter.
commitDetails.get(adapter)
Look up the adapter's created/updated/deleted trio object, which was instantiated at the top of this method here. It's simply an object with the 3 properties created, updated, and deleted, whose values are empty OrderedSets.
[get(record, 'dirtyType')]
Get the appropriate OrderedSet from the object we just obtained. For example, if the record we're on has been updated, get(record, 'dirtyType') will return the string updated. The brackets are just standard JavaScript property lookup, and so it grabs the updated OrderedSet from our trio object in the previous step.
.add(record);
Finally, add the record to the OrderedSet. On subsequent iterations of the loop, we'll add other records of the same type, so all created records get added to one set, all updated records get added to another set, and all deleted records get added to the third set.
What we end up with at the end of the entire method and return from the property is a Map whose keys are adapters, and whose values are these objects with the 3 properties created, updated, and deleted. Each of those, in turn, are OrderedSets of all the records in the transaction that have been created for that adapter, updated for that adapter, and deleted for that adapter, respectively.
Notice that this computed property is marked volatile, so it will get recomputed each time that someone gets the commitDetails property.