Non-persistent attributes in EmberJS - ember.js

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.

Related

How to access top level meta key from json-api server's 200 response when calling ember data's destroyRecord()

I'm working on an Ember app that is using Ember Data and the now default json-api adapter.
According to the json-api spec (http://jsonapi.org/format/#crud-deleting) when deleting a record your server should return a 200 response if the deletion is successful and the server responds with just a top level meta key.
My current server does just this and I'm trying to figure out how to access the data in the top level meta object when using Ember Data's model.destroyRecord() method.
myModel.destroyRecord().then(function(model){
// the returned value is the model. How can I get the actual metadata
// returned by the server?
});
The server response contains information about what exactly was deleted and looks like this:
{
"meta": {
num-deleted-a: 10,
num-deleted-b: 100,
num-deleted-c: 200
}
}
I'd like to get this information so I can display it to the user.
Thank you!
I am using the following versions:
Ember : 2.2.0
Ember Data : 2.3.3
jQuery : 1.11.3
After upgrading to Ember 2.6.1 and Ember 2.6.1 I was no longer able to access the store._metadataFor property.
To get access to the metadata from a particular call I now override the serializer for the model and add a meta property to the model itself that simply passes through the metadata.
As an example I have a record type called vote which when saved returns some metadata.
For the model I do the following:
// Vote Model (/app/models/vote)
export default DS.Model.extend({
vote: DS.attr('number'),
// Since i don't provide a transform the values for meta are passed through in
// raw form
meta: DS.attr()
});
Then in the serializer for the vote model I do the following:
// Vote serializer (/app/serializers/vote)
import DS from "ember-data";
export default DS.JSONAPISerializer.extend({
normalizeSaveResponse(store, primaryModelClass, payload, id, requestType) {
// The metadata in the payload does get processed by default and will be
// placed into a top level `meta` key on the returned documentHash
let documentHash = this._super(store, primaryModelClass, payload, id, requestType);
// Make sure we always have an empty object assigned to the meta attribute
if(typeof(payload.meta) !== 'object'){
payload.meta = {};
}
// Move the metadata into the attributes hash for the model
documentHash.data.attributes.meta = payload.meta;
return documentHash;
}
});
Note that in the above example I'm only adding in metadata to the vote model when making a save call to the store. If you wanted to always add in the metadata then you would override the normalize method instead of the normalizeSaveResponse method.
Then you can access a meta field in the results of your save call.
let vote = self.store.createRecord('vote', {
vote: voteValue
});
vote.save().then(function(result){
// this will now contain your metadata
console.info(result.get('meta'));
});
Ember does not support meta for single model requests (find,save and destroyRecord) at the moment!
If you want this you have to hook into ember internals.
The following code uses ember internals from ember 2.3 and may break in future versions!
There is the undocumented _metadataFor function on the store that gives you the last metadata for a given type. I use a custom initializer to always save it to the Model:
import Ember from 'ember';
import DS from 'ember-data';
const {set} = Ember;
export function initialize(application) {
DS.Model.reopen({
meta: null,
didCommit() {
this._super(...arguments);
set(this, 'meta', this.store._metadataFor(this.constructor.modelName));
}
});
};
export default {
name: 'meta',
initialize: initialize
};
After this you can do model.save().then(() => console.log(model.get('meta'))) or model.destroyRecord.then(() => console.log(model.get('meta'))).
Maybe checkout this ember-twiddle.

How do I save an Ember Data record without submitting it to the backend?

So Ember Data Model has a deleteRecord() that performs a destroyRecord() without submitting it to the backend.
How do I do save() without submitting it to the backend?
The reason I need it is that I'm using a custom service to batch-save multiple records of different types (models) in one request. I'm successfully sending the requests, and records are persisted on the backend.
But as the request does not go through the Ember Data pipeline, the response from server will be discarded unless I handle it manually.
Basically, I have this in a service:
// Accepts an array of records of mixed types,
// both existing and new
batchSave (records) {
this
.customAjax(records) // The records are persisted
.then(payload => { // Response from the backend with updated records
store.pushPayload(payload); // Now all records have been updated with their current state
// Now all the records are in their current state.
// But they are still dirty!
// How do I mark them clean and saved
});
I've seen this but it seems to discard dirty attributes, while I want dirty attributes to become clean.
I've also tried store.didSaveRecord() but after it records are still dirty.
This is an extension of #Tom Netzband's proposal with a big more sugar.
First, a mixin for adapters:
// mixins/prevent-save-adapter.js
export default Ember.Mixin.create({
preventSave: false,
updateRecord(store, type, snapshot) {
if (!this.get('preventSave'))
return this._super(store, type, snapshot);
this.set('preventSave', false);
return true;
}
});
Then one for models:
// mixins/prevent-save-model.js
export default Ember.Mixin.create({
saveWithoutSave() {
var modelName = this.constructor.modelName;
var adapter = this.adapterFor(modelName);
adapter . set('preventSave', true);
return this.save();
}
});
The post adapter:
// adapters/post.js
export default ApplicationAdapter.extend(PreventSaveAdapter);
And the post model:
// models/post.js
export default DS.Model.extend(PreventSaveModel, {
...
);
Using this:
// controllers/some-controller.js
export default Ember.Controller.extend({
actions: {
someAction () {
(...)
post.saveWithoutSave();
}
}
});
Untested.
Disclaimer: This isn't an ideal solution and I hope someone can point us both in a better direction.
Edit: torazaburo's solution on this thread seems like the best way to go.
I've run into the same situation and haven't found a great solution. I ended up writing a custom adapter and added a service to just return true in updateRecord if the service had a flag of preventRequest: true.
Example:
// services/prevent-request.js
export default Ember.Service.extend({
prevent: false // default
});
// adapters/post.js
export default ApplicationAdapter.extend({
preventSave: Ember.inject.service(),
updateRecord (store, type, snapshot) {
if (this.get('preventSave.prevent')) {
this.set('preventSave.prevent', false);
return true;
}
this._super(store, type, snapshot);
}
});
// controllers/some-controller.js
export default Ember.Controller.extend({
preventSave: Ember.inject.service(),
actions: {
someAction () {
(...)
this.get('preventSave').set('prevent', true);
post.save();
}
}
});
According to Ember Guides using store.createRecord() will created the record and add it to the store but it won't make a request to the backend.
Example:
store.createRecord('post', {
title: 'Rails is Omakase',
body: 'Lorem ipsum'
});
The store object is available in controllers and routes using this.store.
Then if you want to persist it, just call save().
Example:
post.save(); // => POST to '/posts'
isDirty means that the record has local changes that have not yet been saved by the adapter. This includes records that have been created (but not yet saved) or deleted.
Dirty states have three child states:
uncommitted: the store has not yet handed off the record to be saved.
inFlight: the store has handed off the record to be saved, but the adapter has not yet acknowledged success.
invalid: the record has invalid information and cannot be send to the adapter yet.
If you want to make a record clean, try something like this (untested by me):
record.get('stateManager').send('becameClean');

Using primary keys with Ember Data

I've been struggling for the past few days with primary keys and the last version of Ember Data.
I first read how to do it on the Breaking Changes file on GitHub, but it's apparently outdated. I tried several other ways (with the help of Peter Wagenet on IRC), but none of them seem to work.
I would like to make slug my primary key on my model, and also since I'm working with MongoDB, I would like to use _id instead of id.
Has anyone figured out how to do this? My underlying problem is that model records get loaded twice when I do several App.MyModel.find() on the model.
As of Ember Data 1.0 beta you define primaryKey on the respective serializer.
For the entire app
App.ApplicationSerializer = DS.RESTSerializer.extend({
primaryKey: '_id'
});
For a single type
App.FooSerializer = DS.RESTSerializer.extend({
primaryKey: '_id'
});
You will still refer to it as id on the model, but Ember Data will serialize/deserialize it to _id during transfer.
Example: http://emberjs.jsbin.com/OxIDiVU/635/edit
Read More about it here: http://emberjs.com/api/data/classes/DS.RESTSerializer.html#property_primaryKey
I would like to make slug my primary key on my model, and also since I'm working with MongoDB, I would like to use _id instead of id.
Use the adapter's map API to specify the attribute that should be used as primary key:
App.Adapter.map('App.Person', {
primaryKey: '_id'
});
See serializer.js api docs for detail. If you need to further customize how the records id is serialized, use the addId hook described here.
Since ember-data is still under active development, documentation on this feature is somewhat limited and may change before 1.0 release. In the meantime refer to the ember-data test suite to see examples of this in action. For example:
mapped primary keys are respected when serializing a record to JSON demonstrates how an _id attribute will be included in json when a record is saved
mapped primary keys are respected when materializing a record from JSON shows how JSON with _id attribute will be transformed into a record with the correct id
In case the solution suggested by Nikita doesn't work (didn't for me using revision 11 of ember-data), here is how I changed the primary key when working with the RESTAdapter:
App.Adapter = DS.RESTAdapter.extend({
serializer: "App.MySerializer"
});
// notice we extend the RESTSerializer, not Serializer!
App.MySerializer = DS.RESTSerializer.extend({
primaryKey: function(type) {
return '_id'; // you get it...
}
});
I'm aware that this probably won't help the OP anmore but I still post it as it may be helpful for some of you.
Try to extend your adapter like this:
App.RESTSerializer = DS.RESTSerializer.extend({
init: function() {
this._super();
this.map('App.MyModel', {
primaryKey: "_id"
});
}
});
I use MongoDB and Ember-Data 1.0.0-beta.6 in my application and the _id posed a problem in Ember 1.4.0 for me too. Here's what I've done to solve the problem, assuming the returned JSON array is nested in the root property "people":
App.ApplicationSerializer = DS.RESTSerializer.extend({
normalizeHash: {
people: function(hash) {
hash.id = hash._id;
delete hash._id;
return hash;
}
}
});
This is, of course, an application-wide serializer but you can limit it to a specific path with something like App.MyPathSerializer = DS.RESTSerializer.extend({ ... });

Ember: unsaved models in dynamic URLs

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.

How to remove duplicate records in ember-data during addObjects

I'm working with a set of data that can potentially have duplicate values. When I initially add the data I'm using what little information I have available on the client (static info stored on the model in memory).
But because I need to fetch the latest each time the handlebars template is shown I also fire off a "findAll" in the computed property to get any new data that might have hit server side since the initial ember app was launched.
During this process I use the "addObjects" method on the ember-data model but when the server side is returned I see duplicate records in the array (assuming it's because they don't have the same clientId)
App.Day = DS.Model.extend({
appointments: function() {
//this will hit a backend server so it's slow
return App.Appointment.find();
}.property(),
slots: function() {
//no need to hit a backend server here so it's fast
return App.Slot.all();
}.property(),
combined: function() {
var apts = this.get('apppointments'),
slots = this.get('slots');
for(var i = 0; i < slots.get('length'); i++) {
var slot = slots.objectAt(i);
var tempApt = App.Appointment.createRecord({start: slot.get('start'), end: slot.get('end')});
apts.addObjects(tempApt);
}
return apts;
}.property()
});
Is it possible to tell an ember-data model what makes it unique so that when the promise is resolved it will know "this already exists in the AdapterPopulatedRecordArray so I'll just update it's value instead of showing it twice"
You can use
DS.RESTAdapter.map('App.Slot', {
primaryKey: 'name-of-attribute'
});
DS.RESTAdapter.map('App.Appointment', {
primaryKey: 'name-of-attribute'
});
But I think it is still impossible because App.Slot and App.Appointment are different model classes, so if they have same ids it won't help. You need to use the same model for both slots and appointments for this to work.
Edit
After examinig the source of ember-data, i think that you can define the primaryKey when you define your classes, like:
App.Slot = DS.Model.extend({
primaryKey: 'myId',
otherField: DS.attr('number')
});
I didn't tested it though..
Edit 2
After further reading seems that the previous edit is no longer supported. You need to use map as i wrote earlier.