I've got backbone-relational working fairly well so far. I have relationships and reverse relationships well established (see below). When I initially call .fetch() on my Country model instance, the nominees array is parsed out into nominee models perfectly.
When I call .fetch() again later, however, these related models do not update, even though the nominee data has changed (e.g. the vote count has incremented). Essentially it seems that Backbone's .set() method understands relationships initially but not subsequently.
Country Model
var Country = Backbone.RelationalModel.extend({
baseUrl : config.service.url + '/country',
url : function () {
return this.baseUrl;
},
relations : [
{
type : Backbone.HasMany,
key : 'nominees',
relatedModel : Nominee,
collectionType : Nominee_Collection,
reverseRelation : {
key : 'country',
includeInJSON : false
}
}
]
});
JSON Response on country.fetch()
{
"entrant_count" : 1234,
"vote_count" : 1234,
"nominees" : [
{
"id" : 3,
"name" : "John Doe",
"vote_count" : 1,
"user_can_vote" : true
},
{
"id" : 4,
"name" : "Marty McFly",
"vote_count" : 2,
"user_can_vote" : true
}
]
}
Any help would be greatly appreciated, as always.
So it appears that backbone-relational specifically forgoes updating relations automatically (see the updateRelations method), and simply emits a relational:change:nominees event which your models can target. If, however, you wish to programmatically update your related models, simply modify the updateRelations method as follows:
Backbone.RelationalModel.prototype.updateRelations = function( options ) {
if ( this._isInitialized && !this.isLocked() ) {
_.each( this._relations || [], function( rel ) {
// Update from data in `rel.keySource` if set, or `rel.key` otherwise
var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ];
if ( rel.related !== val ) {
this.trigger( 'relational:change:' + rel.key, this, val, options || {} );
// automatically update related models
_.each(val, function (data) {
var model = rel.related.get(data.id);
if (model) {
model.set(data);
} else {
rel.related.add(data);
}
});
}
}, this );
}
};
(Note that this does not handle deletion of models from a collection, only updates to existing models, and the addition of new models to a collection)
Related
I am trying to map data so that elements only get re-rendered when values have actually changed.
{
Apps : [
{
"Categories" : [{
"Name" : "#Some,#More,#Tags,#For,#Measure"
}
],
"Concentrator" : "",
"Health" : 1,
"Id" : 2648,
"Ip" : "1.1.1.1",
"IsDisabled" : true,
"IsObsolete" : false,
"Name" : "",
"Path" : "...",
"SvcUrl" : "http://1.1.1.1",
"TimeStamp" : "\/Date(1463015444163)\/",
"Type" : "...",
"Version" : "1.0.0.0"
}
...
]
...
}
var ViewModel = function() {
self.Apps = ko.observableArray([]);
}
var myModel = new ViewModel();
var map = {
'Apps': {
create: function (options) {
return new AppModel(options.data);
},
key: function(data) { return ko.utils.unwrapObservable(data.Id); }
}
}
var AppModel = function(data){
data.Categories = data.Categories[0].Name.split(',');
ko.mapping.fromJS(data, { }, this);
return this;
}
function UpdateViewModel() {
return api.getDashboard().done(function (data) {
ko.mapping.fromJS(data, map, myModel);
});
}
loopMe(UpdateViewModel, 5000);
function loopMe(func, time) {
//Immediate run, once finished we set a timeout and run loopMe again
func().always(function () {
setTimeout(function () { loopMe(func, time); }, time);
});
}
<script type="tmpl" id="App-template">
<div>
<!-- ko foreach: Categories -->
<span class="btn btn-default btn-xs" data-bind="text:$data"></span>
<!-- /ko -->
</div>
</script>
On the first run of UpdateViewModel I will see 5 spans as expected. On the second call, receiving the same data, it gets updated to a single span that says [Object object] which is because it still thinks Categories is an array of objects instead of an array of strings.
Everything seems fixed if I change 'create' to 'update' in my map, however it seems that the spans are then re-rendered every time regardless if data changed or not.
Can anyone lend me a hand in the direction I need to go so that I can
adjust the Categories array from objects to strings
Only re-render/render changed/new items
Here is a Fiddle showing the behavior
The problem is with these lines:
var AppModel = function(data){
data.Categories = data.Categories[0].Name.split(','); // <-- mainly this one
ko.mapping.fromJS(data, { }, this);
return this;
}
There's two problems:
You mutate the data object which (at least in our repro) mutates the original object that data references to. So first time one of the fakeData objects is passed in, that one is mutated in place, and will forever be "fixed".
You mutate it in the AppModel constructor function, which is only called the first time. According to your key function, the second time the constructor should not be called, but instead ko-mapping should leave the original object and mutate it in place. But it will do so with a "wrongly" formatted data.Categories property.
The correct fix seems to me to be in your data layer, which we have mocked in the repro, so it makes little sense for my answer to show you how.
Another more hacky way to do this would be to have an update method in your mapping like so:
update: function(options) {
if (!!options.data.Categories[0].Name) {
options.data.Categories = options.data.Categories[0].Name.split(',');
}
return options.data;
},
When it encounters an "unmodified" data object it'll do the same mutation. See this jsfiddle for that solution in action.
This code uses a loop to $unset the "checked" property of all embedded documents in the "value" array, then $set the one when a condition evaluates to true.
But when the condition is true, the update block failed to update the embedded document by setting a checked: "checked", I know that because meteor:PRIMARY> db.radioOptions.find({}).pretty(); gives the same results before and after.
What am I doing wrong? and how to fix it? Thanks
meteor:PRIMARY> db.radioOptions.find({}).pretty();
{
"_id" : "jXQcsXtedQYotKQXG",
"name" : "optionType",
"value" : [
{
"name" : "1stOption",
"caption" : "1st Option"
},
{
"name" : "2ndOption",
"caption" : "2nd Option"
}
]
}
var doc = RadioOptions.findOne({name: obj.name});
if (typeof doc != 'undefined') {
doc.value.forEach(function (embdoc) {
console.log(embdoc);
RadioOptions.update(
{name: obj.name, 'value.name': obj.value},
{$unset: {'value.$.checked': ""}}
);
if (embdoc.name == obj.value) {
console.log(obj.name + " " + obj.value); //obj.value = 1stOption for example
RadioOptions.update(
{name: obj.name, 'value.name': obj.value}, //obj.name = "optionType"
{$set: {'value.$.checked': "checked"}}
);
}
})
}
Let's say that is was your objective to set the array element with the name "2ndOption" to "checked" and $unset all other array elements. You would then instead do:
var doc = RadioOptions.findOne({name: obj.name});
if (typeof doc != 'undefined') {
// You have to update every element
doc.value.forEach(function (embdoc) {
RadioOptions.update(
{ "_id": doc._id, "value.name": embdoc.name },
{ "$unset": { "value.$.checked": "" } }
)
});
// Then just set the one you want
RadioOptions.update(
{ "_id": doc._id, "value.name": "2ndOption" }, // <-- actually using a variable of course
{ "$set": { "value.$.checked": "checked" } }
)
}
Now if you had actually read all the responses on the duplicate question you were given for your original question:
How to Update Multiple Array Elements in mongodb
Then you would have seen the response there that mentioned the best way to handle all these mutliple updates was using the "Bulk" API methods in the underlying driver. And also has some useful tips on the general process of updating multiple elements.
On the "server" (not in minimongo) is the best place to to this, and all meteor collections have a .rawCollection() method which returns the collection object from the underlying node native driver.
So the general logic is:
Loop all array elements and update to "turn off"
Match the element you want to "turn on"
And best done in Bulk rather than responding back and forth for each update.
My problem is that I am just starting out with Backbone.js and are having trouble wrapping my head around a complex problem. I want to save a form that have infinite fields, and some of the fields also needs to have infinite options. I'm just worried I might have started at the wrong end with a JSON response, instead of building the models/collections first. Here is a short pseudocode of what I try to achieve.
id:
parent: <blockid>
fields: array(
id:
title:
helpertext
options: array(
id:
type:
value:
)
)
Currently I am working with a faked JSON response from the server, which I built from scratch, and now I want to divide it into models and collections on the client side.
//Fake a server response
var JSONresponse = {
"formid":"1",
"fields":[
{
"fieldid":"1",
"title":"Empty title",
"helper":"Helper text",
"type":"radio",
"options":[
{
"optionid":"1",
"value":"Empty option.."
},
{
"optionid":"2",
"value":"Empty option.."
}
]
},
{
// fieldid2
}
]
};
The idea is to add fields as I see fit, and then if the field type is radio/checkbox/ul/ol there must also be an "options" array within the field.
My work so far:
var app = {};
app.Models = {};
app.Collections = {};
app.View = {};
app.Models.Option = Backbone.Model.extend({
});
app.Collections.Options = Backbone.Collection.extend({
model: app.Models.Option
});
app.Models.Field = Backbone.Model.extend({
options: new app.Collections.Options()
});
app.Collections.Fields = Backbone.Collection.extend({
model: app.Models.Field
});
app.Models.Form = Backbone.Model.extend({
formid : "1",
fields: new app.Collections.Fields(),
initialize: function() {
}
});
How do I split up my JSON response into all these models and collections?
(Perhaps I should re-evaluate my approach, and go for something like form.fieldList and form.optionList[fieldListId] instead. If so, how would that look like?)
Edit: Here is a little jsfiddle after many fixes, but I still don't really know how to make the inner options list work.
The easiest solution would be using Backbone Relational or Backbone Associations.
The documentation should be enough to help you get started.
If you don't want to use a library you could override the parse function on the Form model.
app.Models.Form = Backbone.Model.extend({
defaults: {
fields: new app.Collections.Fields()
},
parse: function(response, options) {
return {
formid: response.formid,
fields: new app.Collections.Fields(_.map(response.fields, function(field) {
if (field.options) {
field.options = new app.Collections.Options(field.options);
}
return field;
}))
};
}
});
Now if you fetch a form from the server, the response will be parsed into an object graph of models and collections.
form.get('fields') will return an app.Collections.Fields collection. form.get('fields').first().get('options') will return an app.Collections.Options collection, if any options exist.
Also, you could create the form model like this:
var form = new app.Models.Form(JSONresponse, {
parse: true
});
This would result in the same object structure.
It's quite hard to handle the case of nested models and collections right in plain Backbone.
Easiest way of handling this will be something like this:
var Option = Nested.Model.extend({
idAttribute : 'optionid',
defaults : {
optionid : Integer
value : ""
}
});
var Field = Nested.Model.extend({
idAttribute : 'fieldid',
defaults : {
fieldid : Integer,
title : "",
helper : "",
type : "radio",
options : Option.Collection
}
});
var Form = Nested.Model.extend({
idAttribute : 'formid',
defaults : {
formid: Integer,
fields: Field.Collection
});
https://github.com/Volicon/backbone.nestedTypes
And that's it. Yep, you'll get direct access to the attributes as free bonus, just form.fields.first().options.first().value, without that get and set garbage.
I've posted the following question which has been answered correctly:
MongoDB - Updating only $ref from DBRef field type
Despite of this when I execute the find method like this:
{ "codeId" : { "$ref" : "code" , "$id" : { "$oid" :
"4ff1c08c6ef25616ce21c4b6"}} }
The document isn't returned... Any idea why?
After the update the document is stored like this:
{ "_id" : { "$oid" : "5097ae1cd3159eb52d05574c"} , "codeId" : { "$ref"
: "code" , "$id" : { "$oid" : "4ff1c08c6ef25616ce21c4b6"}} }
By the way, using uMongo GUI, if I select the Update option over this stored document, and save it, without making any changes whatsoever, and then make the find query once again, the document is returned by the query...
Thanks
This is a clearly one of those DBRef "tweaky" things...
As a temporary (but probably correct) fix, I managed to solve this problem executing this javascript procedure:
var cursor = db.menu.find( { "codeId.$ref" : "version" } );
while( cursor.hasNext() )
{
var document = cursor.next();
db.menu.update(
document,
{ $set: {"codeId" : DBRef("code", document.codeId.$id) }},
{ upsert: false, multi: true }
);
}
Still, I won't consider this to be the best way to achieve what I want... Any other solution that involves less lines?
I have a json store that returns values in json format. Now I need to get the number of rows/records in the json string but when I use store.getCount() function it returns 0, but the combobox is populated with rows, and when I use store.length I get undefined, probably because its not an array anymore, its returning from store, which is calling php script. Anyways, whats the best approach for this problem?
Try this out:
var myStore = Ext.extend(Ext.data.JsonStore, {
... config...,
count : 0,
listeners : {
load : function(){
this.count = this.getCount();
}
}
Ext.reg('myStore', myStore);
and then use inside panels:
items : [{
xtype : 'myStore',
id : 'myStoreId'
}]
Whenever you need to get the count then you can simply do this:
Ext.getCmp('myStoreId').count
Your Json response from server, can be something like this...
{
"total": 9999,
"success": true,
"users": [
{
"id": 1,
"name": "Foo",
"email": "foo#bar.com"
}
]
}
Then you can use reader: {
type : 'json',
root : 'users',
totalProperty : 'total',
successProperty: 'success'
} in your store object.
As from docs if your data source provided you can call getTotalCount to get dataset size.
If you use ajax proxy for the store, smth like
proxy : {
type : 'ajax',
url : 'YOUR URL',
reader : {
type : 'json',
root : 'NAME OF YOUR ROOT ELEMENT',
totalProperty : 'NAME OF YOUR TOTAL PROPERTY' // requiered for paging
}
}
and then load your store like
store.load();
There will be sent Ajax asynchronous request, so you should check count in callback like this
store.load({
callback : function(records, operation, success) {
console.log(this.getCount()); // count considering paging
console.log(this.getTotalCount()); // total size
// or even
console.log(records.length); // number of returned records = getCount()
}
});