Ember Data not creating an id for new instances - javascript

I created a prototype of an Ember app using the Local Storage adapter.
I am now trying to convert the app to use the Ember Data REST adapter with a back-end store.
In the local storage version of the app, Ember generates an id for a new record prior to saving it (and also even if the record is never saved).
For example, in my local storage app, I can log the id in both places
var gecko = this.store.createRecord('gecko', {
date: new Date(),
type: "gecko",
});
console.log(gecko.id, "gecko.id before save");
gecko.save();
console.log(gecko.id, "gecko.id");
By contrast, in the version of the app I'm making with the REST adapter for the back-end store, the id is not logged.
When I check the data Ember is sending to the server, the id is not included (probably because an id was never generated).
Here is the json that Ember is sending to my server
gecko: { type: "alloc", date: "2015-05-30T13:28:27.539Z"}
I am assuming that I am supposed to save the id that Ember generates on my server (which would of course allow it to retrieve the record by id provide my server implements that).
Question: why is there no id being generated?
this is the code
App = Ember.Application.create();
App.Router.map(function() {
this.route("gecko", { path: "/" });
});
App.ApplicationAdapter = DS.RESTAdapter.extend({
//haven't actually any created any code for this part yet
});
App.ApplicationStore = DS.Store.extend({
adapter: App.ApplicationAdapter.create()
});
App.Gecko = DS.Model.extend({
type: DS.attr('string'),
date: DS.attr('date')
})
App.GeckoRoute = Ember.Route.extend({
model: function() {
//currently does nothing. originally I tried to do `return this.store.find('gecko') but since there are no records yet on the backend, it's returning null which leads to an error which Array cannot map over
},
});
App.GeckoController = Ember.Controller.extend({
actions: {
createGeckoButtonClicked: function(){
var gecko = this.store.createRecord('gecko', {
date: new Date(),
type: "gecko",
});
console.log(gecko.id, "gecko.id before save"); //null
gecko.save();
console.log(gecko.id, "gecko.id"); //null
}
}
Note—I'm not sure if it's relevant, but I feel like I'm in a chicken/egg situation with the route because I can't return any entries before I have created them.
So therefore, I'm trying to setup the Ember app to be able to POST an entry to the server, then I will implement the route to retrieve it using return this.store.find('gecko').

When you use the RESTAdapter and save a model, ember-data expects for a valid payload that includes a unique id generated by your backend.
var gecko = this.store.createRecord('gecko', {
date: new Date(),
type: "gecko",
});
/*
Here ember-data expects a payload like this:
gecko: {id: 1, date: "", type: "gecko"}
The id is generated by your backend
*/
gecko.save().then(function(gecko){
console.log(gecko.get('id'))
})

Ember Data doesn't create ids, there isn't anything to stop it from generating non-unique ids. It isn't the source of truth when it comes to gecko records, your database is, so id generation belongs to the db. This is where POST vs PUT comes into play. I want to POST a new gecko record to /api/geckos or I want to PUT gecko record 123 into its place at /api/geckos/123.
If there are no entries in the database you should still be returning a valid response:
{
geckos: []
}
And two other quick things, you should be using getters/setters for property fetching and setting.
var gecko = this.store.createRecord('gecko', {
date: new Date(),
type: "gecko",
});
console.log(gecko.get('id'), "gecko.id before save");
var promise = gecko.save();
And save is an asynchronous process that returns a promise which you can wait to avoid race conditions.
promise.then(function(geckoRecord){
// geckoRecord and geck are the same here, but it's good to know
// it resolves the record
console.log(gecko.get('id'), "gecko.id after save");
console.log(geckoRecord.get('id'), "gecko.id after save");
});

Related

Stop emberjs to send request to server for loaded record

I am having a customer model and a product model
customer model
import DS from 'ember-data';
export default DS.Model.extend({
type: DS.attr('string'),
name: DS.attr('string'),
product: DS.hasMany('product')
});
product model
import DS from 'ember-data';
export default DS.Model.extend({
type: DS.attr('string'),
name: DS.attr('string'),
customer: DS.belongsTo('customer')
});
I load all the customer in model() hook of route.js as below
model() {
customers: this.store.findAll('customer'),
}
so as of now I have all the records of customer.
then using findRecord method I load perticular product as
this.store.findRecord('product', productId);
I call this on click of a button say Find Products, this is written in controller.js
now when I write in template as
{{product.customer.name}}
it send a get request to load the customer which is already loaded. I want to stop that request how I can do this ?
As per my knowledge
When we already have the record, store.findRecord will resolve immediately with cached data, but will send a request in the background to update the record and once the record is updated your template will show the new changes.
I want to stop that request.
You problem is probably that ember-data does not know that the requested customer is already in the store. In your own answer you use the url to peek the customer, so I assume your id is your url, and thats probably your problem.
There is to mention that ember-data should work as you expect, so you are doing something wrong. Since you haven't shown your adapter, serializer and response I can not tell you exactly what's wrong, but I can show you where to look to it.
Also to mention is that you have an attribute type. This is probably not what you want, because the type is, as the id, an ember internal and you should not declare it yourself.
There seems also to be an syntax error in your model hook, because you use the : like in a object definition:
model() {
customers: this.store.findAll('customer'),
}
You have to understand that model(){} is a function, and identical to model: function() {}!
What you should do is to load all the customers with findAll, but its essential to wait for the promise to be resolved until you ask for the customer on the product. So you should do something like this:
model() {
return Ember.RSVP.hash({
customers: this.store.findAll('customer'),
product: this.store.find('product', 1),
}).then(h => h.product);
}
I have built a little twiddle with your situation, and as you see there is no request made for the customer, but also the customer is not shown. This is because of how I mock the server response in the adapter:
export default DS.JSONAPIAdapter.extend({
findRecord (store, type, id, snapshot) {
console.log('find product ', id);
return Ember.RSVP.resolve({
data: {
id: '2',
type: 'product',
attributes: { name: 'product-one'},
}
});
}
});
Because the customer relationship is not declared ember-data assumes it has none. Lets fix this by including the customer in the product like this:
return Ember.RSVP.resolve({
data: {
id: '2',
type: 'product',
attributes: { name: 'product-one'},
relationships: {
customer: {
links: {
related: '/customers/1'
}
}
}
}
});
Now I have exact the same situation as you have! Ember ties to fetch the customer it already has. Is is because ember-data does not know that it has the customer, because it only knows his url, not his id. This is also the reason why the background reload hooks don't work for you, because its not a reload for ember-data, its an initial load!
Its pretty easy to fix this by just giving ember-data the information:
customer: {
data: { type: 'customer', id: '1' },
links: {
related: '/customers/1'
}
}
And woha, everything works! Now you could even remove the related link because ember-data could just build it by its own.
So in conclusion the right place to fix this is either to adjust the server response itself, or use the serializer to fix it so ember-data can know if it already has the record.
You can use this.store.peekRecord('product', productId) instead of findRecord. Peek will return results without doing a network request. It will completely rely on what is already in store.
DS.Adapter classes have methods for this specific need (and therefore all other adapters):
shouldBackgroundReloadAll for background reload in store.findAll calls.
shouldBackgroundReloadRecord for background reload in store.findRecord calls.
shouldReloadAll for blocking reload in store.findAll calls.
shouldReloadRecord for blocking reload in store.findRecord calls.
If any of these methods returns false, the store will not reload the record/s/ from the backend.
However I would recommend to have some care if you override them, otherwise I suspect some things may stop working as expected, read the documentation about these methods and do some research about its implications.
I got the solution.
If we return false from shouldBackgroundReloadAll and shouldBackgroundReloadRecord, then also ember will send background request if the model we are looking for is in belongsTo relationship.
In template I am calling customer name
{{product.customer.name}}
It is sending ajax request as it has belongsTo relationship.
I overriding the method findBelongsTo in the adapters/application.js. I am checking whether record is loaded or not if loaded then do not send request
findBelongsTo: function(store, snapshot, url, relationship) {
var record = store.peekRecord(relationship.type, url);
if(record){
return new Ember.RSVP.Promise(function(resolve, reject) {
// on success
var r = Ember.copy(record._internalModel._data, true);
r._id = record.id;
var d ={ meta: {} };
d[Ember.String.pluralize(relationship.type)] = [r];
resolve(d);
// on failure
reject(null);
});
} else{
url = this.buildURL(relationship.type, url, snapshot, 'findRecord');
return this.ajax(url, 'GET');
}
}

How to get single book details from server using backbone model?

I am creating simple book library using backbone.js. Functionalities are very simple.
1.) Get All Books
2.) Get Particular Book (from server)
3.) Display Books
from the collection i can get all the books. But how can I get single book from server using Models ? I need to make ajax call to server to get the book details. since the details may update pretty soon (I don't want to get it from all books collection)
So far I created Views, Model, Collections as follows
var Book: Backbone.Model.extend({
defaults:{
name: "",
id: ""
}
});
var Library: Backbone.Collection.extend({
model: Book,
url: function(){
return "/books";
},
fetchBooks: function(){
fetch({
success: success_callback,
error: error_callback
});
},
fetchBook: function(bookId){
//how to get single book ?
}
});
How do I get single item from the server using my existing models and collections ?
If you need to use your Model outside the scope of a collection, you need to specify the urlRoot property:
var Book: Backbone.Model.extend({
defaults: {
name: ""
// ** See Note on Defaults below ...
},
urlRoot: "/books"
});
After that you can initialize the Model with a given ID and fetch it:
fetchBook: function(bookId){
var book = new Book({ id: bookId });
book.fetch({
success: function() {
console.log('Received book: ' + book.toJSON());
}
});
}
This will result in a GET request to `/books/{id}'. Assuming the server returns JSON, all your results will be populated as attributes on the book instance.
** NOTE on Defaults: You definitely shouldn't have an id default to an empty string, as Backbone uses the presence or absence of an id to determine if a model has been saved on the server or not. For instance if you ever use model.save(...), Backbone will issue a POST if the id does not exist, or a PUT if the id does exist.

Updating a specific record in Ember Data without making HTTP calls?

I have an Ember.js model called emails. This is what the file looks like:
import DS from 'ember-data';
export default DS.Model.extend({
label : DS.attr('string'),
primary : DS.attr('string'),
email : DS.attr('string')
});
I am fetching results from the server and pushing data in by doing something like:
this.store.createRecord('emails', {
label: 'Foo',
primary: true,
email: foo#bar.com
});
I have a form that allows users to add new emails to the store. Once they add a new one, I am pushing it to the store using the above code.
There can only be one record in the store with the key primary' set totrue`. When a user adds a new primary email, I need to manually edit any existing primary email to false and only then add the new email record.
I tried doing something like this.store.peekAll('emails') and parsing it but I can't seem to understand how to fetch only the record where primary: true and update it. How can I accomplish this?
Remember, I am not using the REST Adapter so I cannot query Ember Data like this: http://guides.emberjs.com/v1.13.0/models/finding-records/#toc_querying-for-multiple-records.
You could combine Ember Data's peekAll and the Ember.Enumerable method findBy to get the record:
var primary = this.store.peekAll('email').findBy('primary', true);
You would want to filter the list of emails for that attribute:
emails = this.store.peekAll('email');
primaryEmails = emails.filter(function(email){
return email.get('primary') === true;
});
//because filter will return an array, hopefully of length 1:
primaryEmail = primaryEmails[0];

backbone.LocalStorage with RequireJS

Im trying to get up and running with backbone localStorage via the install below
https://github.com/jeromegn/Backbone.localStorage
but I can't figure out how to actually integrate it into my application. I have connected the collection I want to be stored locally (saved) but Im unsure about how to save/fetch models (upon reload).
(function($){
require.config({
paths: {
jquery: "//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js",
underscore: "//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js",
backbone: "//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js",
localStorage: "backbone.localStorage.js"
}
});
define('saved', ['localstorage'], function(){
var saved = Backbone.Collection.extend({
localStorage: new Backbone.LocalStorage('saved')
});
return saved;
});
//---------SINGLE ENTRY MODEL----------
var Entry = Backbone.Model.extend({
defaults: function(){
return{
word: '',
definition: ''
}
}
});
//------------ENTRY MODEL COLLECTION------------
EntryList = Backbone.Collection.extend({
localStorage: new Backbone.LocalStorage('saved'),
model: Entry
});
//-----INSTANCIATE COLLECTION----
var dictionary = new EntryList();
var saved = new EntryList();
.
.
.
actually you had already using the localStorage when you do something like this:
//------------ENTRY MODEL COLLECTION------------
EntryList = Backbone.Collection.extend({
localStorage: new Backbone.LocalStorage('saved'),
model: Entry
});
you're initiating variables dictionary and saved from the same collection, i guess you only want the saved to be localStorage? if it is, then it would not be what you expected.
It is because when you add the localStorage: new Backbone.LocalStorage('saved') property while extending the Backbone.Collection, this collection will be forever using the localStorage.
to use it to save, you just have to call saved.create({ word: 'abc', definition: 'i dun know'})
to fetch the saved entry, simply saved.fetch()
the example in the source gives a good view
by the way, you can always type "localStorage" to check if it is initiated or having any data on the browser console (i;m using Chrome Devtool)

Saving a model in local storage

I'm using Jerome's localStorage adapter with Backbone and it works great for collections.
But, now I have a single model that I need to save. So in my model I set:
localStorage: new Store("msg")
I then do my saves and fetch. My problem is that everytime I do a refresh and initialize my app a new representation of my model is added to localStorage, see below.
What am I doing wrong?
window.localStorage.msg = {
// Created after first run
"1de5770c-1431-3b15-539b-695cedf3a415":{
"title":"First run",
"id":"1de5770c-1431-3b15-539b-695cedf3a415"
},
// Created after second run
"26c1fdb7-5803-a61f-ca12-2701dba9a09e":{
"0":{
"title":"First run",
"id":"1de5770c-1431-3b15-539b-695cedf3a415"
},
"title":"Second run",
"id":"26c1fdb7-5803-a61f-ca12-2701dba9a09e"
}
}
I ran into same issue. Maybe you have something similar to this
var Settings = Backbone.Model.extend({
localStorage: new Store("Settings"),
defaults: { a: 1 }
});
var s = new Settings;
s.fetch();
I changed to
var s = new Settings({ id: 1 });
localStorage adapter check for id like
case "read": resp = model.id ? store.find(model) : store.findAll(); break;
so 0 or "" for id wont work and it will return all models in one
I'm new to backbone.js too, but it looks like the persistence model is analogous to database tables. That is to say, it's designed to create/delete/read records from a table. The localStorage adapter does the same, so what you are doing there is creating a Msg "table"
in localStorage, and creating a new Msg "record" each time, and the adapter gives each new Msg a unique id.
If you just have one object, it's probably easier to just use localStorage directly. The API is really straight forward:
localStorage.setItem("key","value");
Keep in mind that localStorage only deals with key/value pairs as strings, so you'd need to convert to/from string format.
Take a look a this question for more on doing that:
Storing Objects in HTML5 localStorage

Categories

Resources