Ember: unsaved models in dynamic URLs - ember.js

I'm currently using the FixtureAdapter in my Ember app, but when I switch to the RESTAdapter, my URLs no longer work.
The app is a scorekeeping type thing, and I want users to be able to log all the scores without having to be connected to the Web. After the game is finished they can optionally save all the data to the server.
Now, when Ember wants to route to say, matches/:match_id, the ID isn't there because I didn't commit anything to the server/store, so my models don't yet have an ID and I get URLs like: /match/null/games/null
Is this expected behaviour? And if so, is there a workaround? I thought about using model.clientId and then overriding the model hook for each route to try and fetch the Model from the store using the id when present and falling back to clientId. Any other ideas?
UPDATE March 10, 2013:
The following seems to fit my needs and allows to (for now) forget about moving back and forth between local storage and the REST adapter:
App.Store = DS.Store.extend({
revision: 11,
adapter: DS.RESTAdapter.extend({
namespace: 'api/v1',
bulkCommit: true,
generateIdForRecord: function(store, record) {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
})
});
UUID function taken from: Create GUID / UUID in JavaScript?

If a record hasn't been committed, then it shouldn't have an id yet. Furthermore, a url that serializes the application state of viewing that record doesn't make any sense because the record won't exist in another browser until it is committed. You couldn't just paste the url elsewhere and have it load where you left off.
I think what you really want to do is serialize the application state differently (ie. generate a less specific url) when the record is uncommitted. You can achieve this by overriding the serialize method in your route.
For example,
App.PostRoute = Ember.Route.extend({
serialize: function(model, params) {
if (model && model.get('isNew')) {
// corresponds to path '/post/:post_id'
return { post_id: 'new'}
}
return this._super(model, params);
}
});
Now if you call transitionToRoute('post', post) from your controller, where post is a newly created but uncommitted record, the application state will be serialized to the path /post/new. If you pass it a committed record with an id, it will be serialized as usual.

Related

getting data from non-standard endpoints using ember-data

I have an endpoint /activities which behaves in a typical fashion for a RESTful API using JSON. I can of course do things like the following:
model: function() {
return this.store.find('activity');
}
to return a list of activities. Or I can:
model: function(params) {
return this.store.find('activity', params.id);
}
get a specific Activity record. This works fine. But the API also allows certain filtering to be done using URL parameters. So for instance,
GET /activities/type/exercise
Unlike a full list it would only bring back activities which are associated with "exercise". What is the best way for one to pull back data using ember-data? At a bare minimum I want the data to come back, be associated to the correct model, and be iterable via a `DS.RecordArray or similar object.
Ideally I would also like to avoid making network requests every time I hit this route although possibly this is something that I can't get "out of the box". Still interested if there are any best practices.
Ember can already query for data, by default using query params i.e.
store.find(activities, {type: 'exercise'}); // /activities?type=exercise
you could override findQuery on your activities adapter so that if conforms to your api:
// /adapters/activity.js
import DS form 'ember-data';
export default DS.RESTAdapter.extend({
findQuery: function(store, type, query) {
for(var key in query) break; // get first key in the example 'type'
var path = ['', key, query[key]].join('/');
return this.ajax(this.buildURL(type.typeKey) + path, 'GET');
},
});
I haven't tested this but it should work.

Accessing store in another route

I have this model:
App.Game = DS.Model.extend({
name: attr(),
uri: attr()
});
and this route:
App.GamesRoute = Ember.Route.extend({
model: function() {
return this.store.find('game');
}
});
This works fine, calls the backend server, and stores elements in the store (I've checked with Ember inspector). This is the json I return:
{"games":[{"id":"TicTacToe","name":"TicTacToe","uri":"http://localhost:10000/games/TicTacToe"}]}
Now I have this template for 'games' (snipped):
{{#each game in model}}
{{#link-to 'games.matchlist' game.id}}{{game.uri}}{{/link-to}}
This shows the URI for each game. Now in the games.matchlist route what I would like to do is to search in the store by the game_id received param and get the game URI. The reason is that the server doesn't follow RESTAdapter conventions, and I would like to make a custom AJAX query to that URI myself.
This doesn't work:
App.GamesMatchlistRoute = Ember.Route.extend({model: function(params) {
var store = this.store;
var game = store.find('game', params.game_id)
console.log(game);
console.log("URI: " + game.uri);
at this point, game is an object but it's not an instance of my model. It doesn't have a uri attribute. What am I doing wrong? I'm feeling that I'm missing something obvious.
If you want to get records without hitting the server and you know you already have it in the store, use this.store.getById('game', ID).
I'm on my mobile, but you need to create a GameAdapter and customize I believe the fetch function. Checkout the docs for adapters on the ember site and you should have your answer.
Your other option is to fetch the data from your server and use this.store.pushPayload(data).
Docs here: http://emberjs.com/api/data/classes/DS.Store.html#method_pushPayload
And the adapter docs here: http://emberjs.com/guides/models/customizing-adapters/

How does Ember Data update the model?

I don't understand how ember data updates the model, if you do this
model: function() {
return this.store.find('something');
}
You're not actually giving it any reference to the model, so how does it update or how does it know what to update when it gets the result back from the server?
That particular example is actually requesting a collection (all) of something.
If you are looking for a particular record you would do this.store.find('something', id) where id is some unique identifier.
this.store.find('something', 3)
this.store.find('something', "cool_post")
App.Something = DS.Model.extend({
someAttr = DS.attr()
});
var promise = this.store.find('something', 2);
promise.then(function(record){
//promise is resolved and the record is ready here
console.log(record.get('someAttr'));
}
find will always return a promise. Ember Data will then asynchronously search it's cache or make a call to the server (or fixture data depending on your adapter). Once the data has return it will then create an instance of something (defined above). It will then use the serializer associated with the adapter used and apply the results to the instance of something.
The format your json should come in is like so:
{
"something": {
"id": 1
"someAttr": "Rails is omakase"
}
}
The model hook is a special hook in that if you return a promise to it it will wait and resolve that promise and use the result of the promise instead of the promise for the model of the controller.
See the ember data transition document for additional information: https://github.com/emberjs/data/blob/master/TRANSITION.md

Non-persistent attributes in EmberJS

Does anyone know of a way to specify for an Ember model an attribute which is not persisted?
Basically, we're loading some metadata related to each model and sending that data to Ember via the RESTAdapter within the model. This metadata can be changed in our app, but is done via using an AJAX call. Once the call succeeds, I want to be able to update this value within the model without Ember sticking its nose in this business by changing the model to the uncommitted and doing whatever it does with transactions behind the scenes.
I also have the problem that this metadata, which is not data from the model's database record, is passed by the RESTAdapter back to the server, which doesn't expect these values. I am using a RoR backend, so the server errors out trying to mass-assign protected attributes which aren't meant to be attributes at all. I know I can scrub the data received on the server, but I would prefer the client to be able to distinguish between persistent data and auxiliary data.
So, to the original question: is there any alternative to Ember-Data's DS.attr('...') which will specify a non-persistent attribute?
The other answers to this question work with Ember data versions up to 0.13, and no longer work.
For Ember data 1.0 beta 3 you can do:
App.ApplicationSerializer = DS.RESTSerializer.extend({
serializeAttribute: function(record, json, key, attribute) {
if (attribute.options.transient) {
return;
}
return this._super(record, json, key, attribute);
}
});
Now you can use transient attributes:
App.User = DS.Model.extend({
name: DS.attr('string', {transient: true})
});
These attributes won't be sent to the server when saving records.
When this PR get's merged it will be possible to flag properties as readOnly. But till then there are some workarounds to this, e.g. overriding your addAttributes method in the Adapter and deal with your special properties, here an example how this could look like:
Define your Model by adding the new option readOnly:
App.MyModel = DS.Model.extend({
myMetaProperty: DS.attr('metaProperty', {readOnly: true})
});
and then on the Adapter:
App.Serializer = DS.RESTSerializer.extend({
addAttributes: function(data, record) {
record.eachAttribute(function(name, attribute) {
if (!attribute.options.readOnly) {
this._addAttribute(data, record, name, attribute.type);
}
}, this);
}
});
what this does is to loop over the attributes of your model and when it find's an attribute with the readOnly flag set it skips the property.
I hope this mechanism works for your use case.
Following this answer, to prevent a field from being serialized, override the default serializer for your model:
In app/serializers/person.js:
export default DS.JSONSerializer.extend({
attrs: {
admin: { serialize: false }
}
});
See here for the source PR. This solution works in Ember Data 2, and should work in older versions as well.
Update
This answer is most likely out of date with the current releases of Ember Data. I wouldn't use anything in my answer.
I'm answering this question for reference, and because your comment indicated that the record remains isDirty, but here is my solution for read-only, non-persistent, non-dirty attributes.
Overriding the addAtributes method in your Serializer prevents readOnly attributes from being sent to the server, which is probably exactly what you want, but you need to extend (or reopen) your adapter to override the dirtyRecordsForAttributeChange to prevent the record from becoming dirty:
App.CustomAdapter = DS.RESTAdapter.extend({
dirtyRecordsForAttributeChange: function(dirtySet, record, attrName, newValue, oldValue) {
meta = record.constructor.metaForProperty(attrName);
if (meta && meta.options.readOnly) { return; }
this._super.apply(this, arguments);
};
});
Then you can use readOnly attributes like so:
App.User = DS.Model.extend({
name: DS.attr('string', {readOnly: true})
});
user = App.User.find(1); # => {id: 1, name: 'John Doe'}
user.set('name', 'Jane Doe'); #
user.get('isDirty') # => false
This setup is working for me.

How to embed one-to-one relation in Ember-data?

I'm using Ember 1.0-pre4.
I have two models in one-to-one relationship:
App.Lesson = DS.Model.extend
timeslot: DS.belongsTo 'App.Timeslot'
App.Timeslot = DS.Model.extend
lesson: DS.belongsTo 'App.Lesson'
I have an adapter configured to embed timeslot into lesson upon saving:
App.Adapter = DS.RESTAdapter.extend()
App.Adapter.map App.Lesson,
timeslot: { embedded: 'always' }
App.Store = DS.Store.extend
revision: 11
adapter: App.Adapter.create()
Then I create a lesson and a time slot and try to save them:
lesson = App.Lesson.createRecord
group: group
lesson.set('timeslot', App.Timeslot.createRecord())
lesson.store.commit()
But upon saving nothing is embedded and I see to POST requests, one for lesson and one for timeslot.
How do I tell Ember to always embed timeslot into lesson?
I think this is a bug and you should report it. Sifting through the source code and doing some tests reveals that createRecord does not take into account the embedded configuration at all. This configuration is only used for the serialization and deserialization process.
When you make a call to createRecord, a record is added to a bucket, created, and on commit ember-data simply fires an ajax post on every record in the bucket.
So to get back to your code, to ember-data, you created two records and on commits it will fire an ajax post call for the Lesson object with Timeslot embedded in it, AND will also, in a subsequent call, fires another ajax post for Timeslot, the last remaining record in the bucket.
lesson = QrTimetable.Lesson.createRecord
group: group
lesson.set('timeslot', QrTimetable.Timeslot.createRecord())
lesson.store.commit()
Unless, someone with better understanding of the inners of ember-data contradicts my point, i tend to believe that, again, that this is a bug.
Here's the last code called when you commit the transaction.
createRecord: function(store, type, record) {
var root = this.rootForType(type);
var data = {};
data[root] = this.serialize(record, { includeId: true });
this.ajax(this.buildURL(root), "POST", {
data: data,
context: this,
success: function(json) {
Ember.run(this, function(){
this.didCreateRecord(store, type, record, json);
});
},
error: function(xhr) {
this.didError(store, type, record, xhr);
}
});
},