I'm new to Backbone, and I'm very confused about what's happening when I pass a JSON array (of objects) to a Backbone Collection.
I'm fetching some JSON from a spreadsheet hosted on Google Drive. I'm parsing that data, as the actual data that I want to use in my collection is deeply nested. In my parse function, if I log the length of my desired array, I get 157 (that's correct). I then pass that array into a Backbone Collection, and the length of my collection is 1 (incorrect). It's as though foo.bar.length = 157, but there is only one 'bar' in 'foo', so when I pass foo.bar into the collection, it takes foo.bar and not the contents of foo.bar! Very confused.
Code below...
var table = new TableView();
TableItem = Backbone.Model.extend(),
TableItemCollection = Backbone.Collection.extend( {
model : TableItem,
url : 'https://spreadsheets.google.com/feeds/list/0AjbU8ta9j916dFdjSVg3YkNPUUJnWkZSWjBDWmZab3c/1/public/basic?alt=json-in-script',
sync : function( method, model, options ) {
var params = _.extend( {
type: 'GET',
dataType: 'jsonp',
url: this.url,
processData: false
}, options );
return $.ajax( params );
},
parse : function( resp, xhr ) {
console.log( resp.feed.entry.length ); // THIS LOGS 157
return resp.feed.entry;
}
} ),
TableView = Backbone.View.extend( {
initialize : function ( options ) {
this.collection = new TableItemCollection();
this.collection.on( 'reset', this.parseResponse, this );
this.collection.fetch( {
reset : true,
success : function ( model, response, options ) {
console.log( 'OK' ); // THIS LOGS 'OK'
},
error : function ( model, response, options ) {
console.log( 'ERROR' );
}
} );
},
parseResponse : function () {
console.log( this.collection.length ); // THIS LOGS 1
}
} );
If you dump one of the items returned by Google Spreadsheets, you will see that the data is nested in multiple objects, something like this
{
"id":{"$t":"https://spreadsheets.google.com/feeds/list/..."},
"updated":{"$t":"2013-07-30T12:01:24.000Z"},
"category":[{"scheme":"...","term":"..."}],
"title":{"type":"text","$t":"ACIW"},
"content":{},
"link":[{"rel":"self","type":"application/atom+xml","href":"..."}]
}
In a Fiddle http://jsfiddle.net/nikoshr/kHBvY/
Note how the id property is wrapped in an object "id":{"$t":"https://spreadsheets.google.com/feeds/list/0AjbU8ta9j916dFdjSVg3YkNPUUJnWkZSWjBDWmZab3c/1/public/basic/cokwr"}
Backbone collections don't allow duplicates and duplicates are determined based on their id. All your items are considered duplicates and collapsed into one. If you remove the id or disambiguate it, you will get your 157 items. For example,
parse : function( resp, xhr ) {
var data = resp.feed.entry, i;
console.log(data.length); // THIS LOGS 157
for (i=data.length-1; i>=0; i--)
data[i].id = data[i].id['$t'];
return data;
}
http://jsfiddle.net/nikoshr/kHBvY/2/ for a demo
You probably will have to unwrap all the attributes to use them in a non hair pulling way.
Related
I'm trying to deserialize a paginated end point. The return request for this end point looks like
{
count: number,
next: string,
previous: string,
data: Array[Objects]
}
The issue I'm having when using js-data to do a findAll, it's injecting this object into the data store. It should be injecting the objects in the data array into the store. So I made a deserialize method on my adapter that looks like this.
deserialize: (resourceConfig:any, response:any) => {
let data = response.data;
if (data && 'count' in data && 'next' in data && 'results' in data) {
data = data.results;
data._meta = {
count: response.data.count,
next: response.data.next,
previous: response.data.previous
};
}
return data;
}
And this works. The array objects are getting injected into my data store. But the meta information is getting lost.
dataStore.findAll('User').then(r => console.log(r._meta)); // r._meta == undefined
I would like to keep that meta information on the returned object. Any ideas?
To do this in v3 you just need to override a couple methods to tweak JSData's
handling of the response from your paginated endpoint. The two most important
things are to tell JSData which nested property of the response are the records
and which nested property should be added to the in-memory store (should be the
same nested property in both cases).
Example:
const store = new DataStore({
addToCache: function (name, data, opts) {
if (name === 'post' && opts.op === 'afterFindAll') {
// Make sure the paginated post records get added to the store (and
// not the whole page object).
return DataStore.prototype.addToCache.call(this, name, data.results, opts);
}
// Otherwise do default behavior
return DataStore.prototype.addToCache.call(this, name, data, opts);
}
});
store.registerAdapter('http', httpAdapter, { 'default': true });
store.defineMapper('post', {
// GET /posts doesn't return data as JSData expects, so we've got to tell
// JSData where the records are in the response.
wrap: function (data, opts) {
// Override behavior of wrap in this instance
if (opts.op === 'afterFindAll') {
// In this example, the Post records are nested under a "results"
// property of the response data. This is a paginated endpoint, so the
// response data might also have properties like "page", "count",
// "hasMore", etc.
data.results = store.getMapper('post').createRecord(data.results);
return data
}
// Otherwise do default behavior
return Mapper.prototype.wrap.call(this, data, opts);
}
});
// Example query, depends on what your backend expects
const query = { status: 'published', page: 1 };
posts.findAll(query)
.then((response) => {
console.log(response.results); // [{...}, {...}, ...]
console.log(response.page); // 1
console.log(response.count); // 10
console.log(response.hasMore); // true
});
My model urlRoot:
urlRoot: function() {
if (this.id != null ) {
return 'notes/' + this.id;
} else return 'notes';
}
Function:
window.show_note = function (note_id) {
var note = new Memo.Models.Note([], { id: note_id });
note.fetch({
success: function (collection, note, response) {
var noteObj = collection.get("0");
var noteView = new Memo.Views.FullNote( {model: noteObj }, {flag: 0 } );
$('.content').html(noteView.render().el);
}
});}
{ id: note_id } - I post this to server to get note by id
I want to do 'set' or 'get' functions on model 'note' after note.fetch in a callback function - success, but only I have is error: 'Uncaught TypeError: note.set is not a function'.
If I do this way: 'var noteObj = collection.get("0");'
I get that I need but I still can`t use get or set.
You should set urlRoot to:
urlRoot: '/notes'
And backbone will figure out that it needs to add the id to the url. (docs)
Assuming Memo.Models.Note is a model and not a collection, the above snippet should be like this:
window.show_note = function(note_id) {
var note = new Memo.Models.Note({ id: note_id });
note.fetch({
success: function (model, response, options) {
var noteView = new Memo.Views.FullNote({
model: model
}, {flag: 0 });
$('.content').html(noteView.render().el);
}
});
};
Note the argument passed to new Memo.Models.Note. A backbone model constructor takes two arguments: attributes and options (docs) as opposed to a collection, which takes models and options (docs). So you'll want to add the hash with the id property as the first argument.
Also note the function signature of the success callback. For a model the success callback takes three arguments: model, response and options (docs). You'll be interested in the model argument because that is the fetched backbone model. response is the raw response data.
I hope my assumptions are right and this is the answer you are looking for.
I am receiving this JSON file back from an AJAX call:
[
{
"LINKNAME": "Annual Training",
"HITS": 1
},
{
"LINKNAME": "In Focus Newsletter",
"HITS": 1
},
{
"LINKNAME": "NITA (secured)",
"HITS": 1
},
{
"LINKNAME": "Your Current Events",
"HITS": 1
},
]
Here is my AJAX call:
$(document).ready(function(e) {
$.ajax({
method: "GET",
url: url,
}).done(function(api) {
console.log(api);
var obj = JSON.parse(api),
totRes = Object.keys(obj).length;
$.each(obj.children, function (index, value) {
alert(value);
});
}).fail(function( jqXHR, textStatus ) {
alert('Service Catalog: Error loading '+jqXHR+' data. Request fail caused by: '+textStatus);
});
});
I need to be able to extract the data from the JSON and use it but since the JSON objects aren't gioven a title then I am unsure how to extarpolate the data inside the inner object. Thanks in advance. Please ask if you do not understand my question.
Your JSON is just an array of plain objects.
To iterate over an array, you can use various methods. Since you're using jQuery, I'll just suggest $.each:
var arr = JSON.parse(api);
$.each(arr, function(i, obj) {
// access whatever property you want... obj[LINKNAME] or whatever
});
You can also use Array.prototype.forEach, or even just your basic for loop:
arr.forEach(function(obj) {
// obj is the current plain object... e.g. { LINKNAME: 'whatever', HITS: 0 }
});
I would also consider paying attention to how you are referring to the objects that you are receiving. While it is true that arrays are objects, and plain objects are objects, I would probably stick to referring to an array as an array, and a plain object as an object. This is because what you are receiving, in the form of JSON, is an array object of plain objects (or more simply, an array of objects).
Calling the array an "object" and referring to it as obj may confuse you when reading through the code quickly (yes, it is a good abstraction for potential extensibility if you end up not always receiving an array, but that's not the case here.)
Also, to once you have access the object in the each loop, you can iterate over the properties of the object if you need to (taken from this answer):
var obj = {
"a": 1,
"b": 2,
"c": 3
};
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
// or if (Object.prototype.hasOwnProperty.call(obj,prop)) for safety...
alert("prop: " + prop + " value: " + obj[prop])
}
}
First, you can add the setting dataType: 'json' when you send a request. This way you'll have api as javascript array.
Then you'll be able to iterate it via javascript for.
$.ajax({
method: "GET",
url: url,
dataType: "json"
}).done(function(api) {
for (var i = 0; i < api.length; i++) {
var name = api[i]["LINKNAME"],
hits = api[i]["HITS"];
// ...
}
// ...
$(document).ready(function(e) {
$.ajax({
method: "GET",
url: url,
}).done(function(api) {
if (api && api.length > 0) {
api.forEach(function (item) {
console.log(item); // logs whole object
console.log('item name %s', item.LINKNAME);
console.log('item hits %s', item.HITS);
});
}
}).fail(function( jqXHR, textStatus ) {
alert('Service Catalog: Error loading '+jqXHR+' data. Request fail caused by: '+textStatus);
});
});
You can filter the results to make sure you're only using objects that contain both 'LINKNAME' and 'HITS' pretty easily:
.done(function(api) {
if (api && api.length > 0) {
var objs = api.filter(function (item) {
return item.hasOwnProperty('LINKNAME') && item.hasOwnProperty('HITS');
});
objs.forEach(function (item) {
console.log(item); // logs whole object
console.log('item name %s', item.LINKNAME);
console.log('item hits %s', item.HITS);
});
}
});
I have a view model setup with an observable array of user objects. The add/remove of items are working correctly but how do I update items? I can find the values using the ko indexOf function.
function User( username, date_inactive, date_active ) {
this.username = username;
this.date_active = date_active;
this.date_inactive = date_inactive;
};
User.prototype.inactivateMe = function() {
json_responses.push( this );
$.getJSON( "url" + this.username, function( json ) {
original = json_response.pop();
//do update here
});
};
userModel = [ ], //Where the loaded usernames are stored.
viewUserModel = {
users: ko.observableArray(userModel)
//.......
//This is how I'm adding users to the array.
addUser: function () {
$.getJSON( "url",
{ username: usern },
function( json ) {
if( json.STATUS != undefined && json.STATUS == 'success' ) {
newuser = new User( json.USERNAME, json.DATE_ACTIVE, json.DATE_INACTIVE );
viewUserModel.users.push( newuser );
}
}
});
}
The values of viewUserModel.users are pushed into the array from a server json reponse.
I want to be able to update the date_active and date_inactive values when the user clicks a button and the server responses with success.
My setup is an adaption from http://net.tutsplus.com/tutorials/javascript-ajax/into-the-ring-with-knockout-js-the-title-fight/
An observable array only tracks changes made to the array (such as pushes and pops), not the data itself. You will need to make date-active and date_inactive observables as #Ianzz has specified.
function User( username, date_inactive, date_active ) {
this.username = username;
this.date_active = ko.observable(date_active);
this.date_inactive = ko.observable(date_inactive);
};
Afterwards in your html, do something such as
<div data-bind="foreach: Users">
<input data-bind="value: date_active"/>
<input data-bind="value: date_inactive"/>
<div>
See fiddle for full example.
I am rolling a localStorage adaper for Ember data and when I run the find function, ie:
App.store.find(App.Person, 0 );
I am getting this error:
Uncaught Error: assertion failed: A data hash was loaded for a model of type App.Person but no primary key 'undefined' was provided.
At a more general level, I am a bit confused about the relationship between 'the persistent layer' ( in this case localStorage ) and the Ember Store. What does it mean to load something to the store? Does it mean an instance of DS.Person model is created with the data ?
Also, if you'll note I commented out a line 'App.store.didCreateRecord( model, data )' in the createRecord method since it, too is not working. What happens if I do not call store.didCreateRecord after the record has been loaded to localStorage.
The details:
DS.LocalStorageAdapter = DS.Adapter.extend({
get: Ember.get,
set: Ember.set,
createRecord: function( store, modelType, model ){
var records, index, data;
// get existing records of this model
records = this.localStorage.get( modelType );
index = records.length;
data = this.get( model, 'data' );
// set storageID of data
data.set( 'storageID', index );
// add data to existing records
records[index] = data;
// encode records
records = JSON.stringify( records );
// store records in localStorage
this.localStorage.set( modelType, records );
// App.store.didCreateRecord( model, data );
},
find: function( store, modelType, id ) {
var records, model;
records = this.localStorage.get( modelType );
model = records[id];
App.store.load( modelType, model );
},
localStorage: {
set: function( modelType, value ){
localStorage.setItem( modelType, value);
},
get: function( modelType ){
var record = localStorage.getItem(modelType);
record = JSON.parse(record) || [];
return record;
}
}
});
//application model
App.Person = DS.Model.extend({
name: DS.attr('string', {key: 'css_name'}),
storageID: DS.attr('number', {defaultValue: 0, key: 'storageID'}),
});
//create a couple of records
App.store.createRecord(App.Person, { ... } );
App.store.createRecord(App.Person, { ... } );
//try and find one record
App.store.find(App.Person, 0);
error thrown:
Uncaught Error: assertion failed: A data hash was loaded for a model of type App.Person but no primary key 'undefined' was provided. ember-latest.js:51
Ember.assert ember-latest.js:51
(anonymous function) ember-latest.js:131
DS.Store.Ember.Object.extend.load ember-data.js:1530
DS.LocalStorageAdapter.DS.Adapter.extend.find bookApp_1.js:111
DS.Store.Ember.Object.extend.findByClientId ember-data.js:1174
DS.Store.Ember.Object.extend.find ember-data.js:1140
findRecord webApplication.js:210
onclick
also please note this is similar to another question posted a bit ago:
Ember Data: A data hash was loaded ... but no primary key 'undefined' was provided
though the answer there seems to not really explain what is going on.
Can give example of loading Fixture data for Ember 1.0 pre2
App.store = DS.Store.create({
revision: 11,
adapter: 'DS.FixtureAdapter'
});
App.User = DS.Model.extend({
email: DS.attr('string'),
password: DS.attr('string')
});
App.User.FIXTURES = [
{
"id": "1",
"email": "test#mail.com",
"password": "password"
}
];
App.User.find(1).get('email')
Works for me.