I have a game model with a scorecards attribute that is a collection. I'm nesting this collection so when I initialize I'm using nestCollection to create the change handlers to keep everything updated and in sync. Whenever I create a new game model, an empty model is added to the scorecards attribute collection but only in memory - what is saved to localstorage is correct. I can't figure out why.
This is my game model definition- Notice the console log statement results:
var Game = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage('datastore'),
defaults: {
name : '',
scorecards: new ScorecardList(),
created : 0
},
initialize : function() {
console.log(this.scorecards); // prints undefined
console.log(this.get('scorecards')); // length is 0 as expected
this.scorecards = nestCollection(this, 'scorecards', new ScorecardList(this.get('scorecards')));
console.log(this.scorecards); // length is 1, with empty element in it
console.log(this.get('scorecards')); // length is 0 as expected
if (this.isNew()) this.set('created', Date.now());
}
});
The nesting code:
function nestCollection(model, attributeName, nestedCollection) {
//setup nested references
for (var i = 0; i < nestedCollection.length; i++) {
model.attributes[attributeName][i] = nestedCollection.at(i).attributes;
}
//create empty arrays if none
nestedCollection.bind('add', function (initiative) {
if (!model.get(attributeName)) {
model.attributes[attributeName] = [];
}
model.get(attributeName).push(initiative.attributes);
});
nestedCollection.bind('remove', function (initiative) {
var updateObj = {};
updateObj[attributeName] = _.without(model.get(attributeName), initiative.attributes);
model.set(updateObj);
});
return nestedCollection;
}
This is the code I use to create a new game:
addGame: function () {
var g = new Game({
name:this.ui.gameName.val()
});
app.gameList.create(g,{wait:true});
//Backbone.history.navigate('game/new/'+ g.id, true);
}
Your problem comes from this piece of code:
new ScorecardList(this.get('scorecards'))
Here you're giving your ScorecardList constructor another collection as argument. This collection happens to be an object. So your collection's constructor will think it's an object you're giving it to create a model.
So basically, this.get('scorecards')) gets cast into a Scorecard (or whatever your model is called), and that's why you have an empty model.
Passing arguments to the constructor for a different purpose than the creation of your collection is a bad idea, you should call a method afterwards.
Related
I'm having trouble trying to get a number from each item in a knockout observable array and add the numbers together and assign it to another computed variable. Here's what I have right now...
Semesters: ko.observableArray([
{
semesterName: "Fall",
semesterCode: "300",
PlannedCourses: ko.observableArray([]),
totalCredits: ko.computed(function(){
var total = 0;
ko.utils.arrayForEach(this.PlannedCourses, function (course) {
total += course.MinHours();
});
return total;
}),
},
...
What I'm trying to do is, in the totalCredits variable, I'm trying to iterate through the PlannedCourses array and get the MinHours variable for each item and add them together in the total variable. Then I return it to the totalCredits item in the Semesters array. The issue I'm having is getting the PlannedCourses variable in the ko.utils.arrayForEach part. I'm getting an undefined on it and I'm not sure why. I think it's a simple syntax error but I can't see what's wrong.
The PlannedCourses observable array is a dynamic object that is getting the list of PlannedCourses properly. It's defined in the context of itself but I'm not passing it to the totalCredits computed function properly.
I hope this is clear enough. Thank you for your help!
Note: All the rest of the code is working as intended. The only part that isn't working is the totalCredits computed function. I'm not sure if anything within the ko.utils.arrayForEach is working as I haven't been able to get that far.
You're going to need to change the way you populate your Semesters observable array to use a constructor function in order to get a reference to the correct scope for this:
function semester(name, code) {
this.Name = name;
this.Code = code;
this.PlannedCourses = ko.observableArray([]);
this.totalCredits = ko.computed(function(){
var total = 0;
ko.utils.arrayForEach(this.PlannedCourses(), function (course) {
//Note the change to "this.PlannedCourses()" above to get the underlying array
total += course.MinHours();
});
return total;
}, this); //now we can pass "this" as the context for the computed
}
See how we can now pass in an object to the second argument for ko.computed to use as the context for this in the inner function. For more information, see the knockout docs: Managing 'this'.
You then create new instances of semester when populating your array:
Semesters: ko.observableArray([
new semester("Fall", "300"),
new semester(...)
]);
This approach also means you have a consistent way of creating your semester objects (the computed is only defined once for one thing), rather than possibly incorporating typos etc in any repetition you may originally have had.
As others already mentioned your this is not what you think it is. In your case the context should be passed to the computed as follows:
totalCredits: ko.computed(function() {
// Computation goes here..
}, this)
Another approach could be to store the correct this to some local variable during the object creation (ex. var self = this; and then use self instead of this).
However, ko.utils.arrayForEach doesn't work with observable arrays but works on pure JavaScript arrays, so you should unwrap the observable array to access the elements of the underlying array:
ko.utils.arrayForEach(this.PlannedCourses(), function(course) {
// ...
});
// Or
ko.utils.arrayForEach(ko.unwrap(this.PlannedCourses), function(course) {
// ...
});
The scope (this) isn't what you think it is.
See http://knockoutjs.com/documentation/computedObservables.html
try adding your context, like the following:
Semesters: ko.observableArray([
{
semesterName: "Fall",
semesterCode: "300",
PlannedCourses: ko.observableArray([]),
totalCredits: ko.computed(function(){
var total = 0;
ko.utils.arrayForEach(this.PlannedCourses, function (course) {
total += course.MinHours();
});
return total;
}, this), // new context passed in here
},
...
Doing this passes in the context of the array item itself into your computed function.
Edit:
you may need to access the Semesters object inside you loop, and add some way to reference the current item:
Semesters: ko.observableArray([
{
semesterName: "Fall",
semesterCode: "300",
PlannedCourses: ko.observableArray([]),
totalCredits: ko.computed(function(){
var total = 0;
for( var i = 0, len = Semesters().length; i < len; i++ ) {
// check current array item, possibly add an id?
if( Semesters()[i].semesterName === "Fall" &&
Semesters()[i].semesterCode === "300" ) {
ko.utils.arrayForEach(Semesters()[i].PlannedCourses, function (course) {
total += course.MinHours();
});
break; // done searching
}
}
return total;
})
},
This jsfiddle ( http://jsfiddle.net/mjmitche/avo5nnus/39/ ) demonstrates the problem I'm having, but I will explain here. I have a main view that has a startInterval and stopInterval function. In the startInterval, I call a method that adds values to a stats array inside a model that I create in the startInterval function with code this.model = new myModel(). If I stop and then startInterval again (at which point the code runs again this.model = new myModel(), it's still the same array from the first press of the startInterval button.
For example, if I press start, and the random number 3 gets added to the stats array (in the addToModel function), and then press stop, and then press start again which adds a 5 to the stats array, the array will actually have a 3, and a 5. You can see the values print to the screen in the jsfiddle if you press start and stop.
In my real application, I've tried to do things like setting the stats array to [] but i can't clear it. Ideally I wish to dereference the view from the model.
startInterval: function(){
this.model = new myModel();
this.model.intervalId = setInterval(this.addToModel.bind(this), 1000);
},
stopInterval: function(){
clearInterval(this.model.intervalId);
var modelstats = this.model.get("stats");
},
addToModel: function(){
var arr = this.model.get("stats");
var num = Math.floor((Math.random() * 10) + 1);
var view = new StatView({model: this.model});
arr.push(num);
this.model.set({"stats" : arr });
}
Any ideas on a solution?
Although my code doesn't show it, the model is eventually getting added to a collection and then saved to a database, so if the only solution involves destroying the model please take that into consideration (i.e. is there a way to destroy a model without removing it from a collection)
your "stats" attributes is shared on the prototype via defaults.
because in javascript arrays and objects are mutable, it references to the same array and doesn't create a new one.
What you can do instead is have the defaults as a function that returns the default - this way they will be unique per instance
var myModel = Backbone.Model.extend({
defaults: function () {
return {
stats: [],
intervalId: ''
}
}
});
IS there a way to check a dirty flag on the model itself, independent of the view?
I need the angular controller to know what properties have been changed, in order to only save changed variables to server.
I have implemented logic regarding if my entire form is dirty or pristine, but that is not specific enough
I could just slap a name and ng-form attribute on every input, to make it recognizable as a form in the controller, but then I end up with a controller that is strongly coupled with the view.
Another not-so appealing approach is to store the initial values that every input is bound to in a separate object, then compare the current values with the initial values to know if they have changed.
I checked Monitor specific fields for pristine/dirty form state and AngularJS : $pristine for ng-check checked inputs
One option I could think of is
As you get a model/object from service, create a replica of the model within the model and bind this new model to your view.
Add a watch on the new Model and as the model changes, use the replica to compare old and new models as follows
var myModel = {
property1: "Property1",
property2: "Property2",
array1:["1","2","3"]
}
var getModel = function(myModel){
var oldData = {};
for(var prop in myModel){
oldData.prop = myModel[prop];
}
myModel.oldData = oldData;
return myModel;
}
var getPropChanged = function(myModel){
var oldData = myModel.oldData;
for(var prop in myModel){
if(prop !== "oldData"){
if(myModel[prop] !== oldData[prop]){
return{
propChanged: prop,
oldValue:oldData[prop],
newValue:myModel[prop]
}
}
}
}
}
You may find it easiest to store and later compare against the JSON representation of the object, rather than looping through the various properties.
See Detect unsaved data using angularjs.
The class shown below may work well for your purpose, and is easily reused across pages.
At the time you load your models, you remember their original values:
$scope.originalValues = new OriginalValues();
// Set the model and remember it's value
$scope.someobject = ...
var key = 'type-' + $scope.someobject.some_unique_key;
$scope.originalValues.remember(key, $scope.someobject);
Later you can determine if it needs to be saved using:
var key = 'type-' + $scope.someobject.some_unique_key;
if ($scope.originalValues.changed(key, $scope.someobject)) {
// Save someobject
...
}
The key allows you to remember the original values for multiple models. If you only have one ng-model the key can simply be 'model' or any other string.
The assumption is that properties starting with '$' or '_' should be ignored when looking for changes, and that new properties will not be added by the UI.
Here's the class definition:
function OriginalValues() {
var hashtable = [ ]; // name -> json
return {
// Remember an object returned by the API
remember: function(key, object) {
// Create a clone, without system properties.
var newobj = { };
for (var property in object) {
if (object.hasOwnProperty(property) && !property.startsWith('_') && !property.startsWith('$')) {
newobj[property] = object[property];
}
}
hashtable[key] = newobj;
},// remember
// See if this object matches the original
changed: function(key, object) {
if (!object) {
return false; // Object does not exist
}
var original = hashtable[key];
if (!original) {
return true; // New object
}
// Compare against the original
for (var property in original) {
var changed = false;
if (object[property] !== original[property]) {
return true; // Property has changed
}
}
return false;
}// changed
}; // returned object
} // OriginalValues
I'm trying to make the {{#each}} helper to iterate over an object, like in vanilla handlebars. Unfortunately if I use #each on an object, Ember.js version gives me this error:
Assertion failed: The value that #each loops over must be an Array. You passed [object Object]
I wrote this helper in attempt to remedy this:
Ember.Handlebars.helper('every', function (context, options) {
var oArray = [];
for (var k in context) {
oArray.push({
key : k,
value : context[k]
})
}
return Ember.Handlebars.helpers.each(oArray, options);
});
Now, when I attempt to use {{#every}}, I get the following error:
Assertion failed: registerBoundHelper-generated helpers do not support use with Handlebars blocks.
This seems like a basic feature, and I know I'm probably missing something obvious. Can anyone help?
Edit:
Here's a fiddle: http://jsfiddle.net/CbV8X/
Use {{each-in}} helper. You can use it like like {{each}} helper.
Example:
{{#each-in modelWhichIsObject as |key value|}}
`{{key}}`:`{{value}}`
{{/each-in}}
JS Bin demo.
After fiddling with it for a few hours, I came up with this hacky way:
Ember.Handlebars.registerHelper('every', function(context, options) {
var oArray = [], actualData = this.get(context);
for (var k in actualData) {
oArray.push({
key: k,
value: actualData[k]
})
}
this.set(context, oArray);
return Ember.Handlebars.helpers.each.apply(this,
Array.prototype.slice.call(arguments));
});
I don't know what repercussions this.set has, but this seems to work!
Here's a fiddle: http://jsfiddle.net/CbV8X/1/
I've been after similar functionality, and since we're sharing our hacky ways, here's my fiddle for the impatient: http://jsfiddle.net/L6axcob8/1/
This fiddle is based on the one provided by #lxe, with updates by #Kingpin2k, and then myself.
Ember: 1.9.1, Handlebars: 2.0.0, jQuery 2.1.3
Here we are adding a helper called every which can iterate over objects and arrays.
For example this model:
model: function() {
return {
properties: {
foo: 'bar',
zoo: 'zar'
}
};
}
can be iterated with the following handlebars template:
<ul class="properties">
{{#every p in properties}}
<li>{{p.key}} : {{p.value}}</li>
{{/every}}
</ul>
every helper works by creating an array from the objects keys, and then coordinating changes to Ember by way of an ArrayController. Yeah, hacky. This does however, let us add/remove properties to/from an object provided that object supports observation of the [] property.
In my use case I have an Ember.Object derived class which notifies [] when properties are added/removed. I'd recommend looking at Ember.Set for this functionality, although I see that Set been recently deprecated. As this is slightly out of this questions scope I'll leave it as an exercise for the reader. Here's a tip: setUnknownProperty
To be notified of property changes we wrap non-object values in what I've called a DataValueObserver which sets up (currently one way) bindings. These bindings provide a bridge between the values held by our internal ArrayController and the object we are observing.
When dealing with objects; we wrap those in ObjectProxy's so that we can introduce a 'key' member without the need to modify the object itself. Why yes, this does imply that you could use #every recursively. Another exercise for the reader ;-)
I'd recommend having your model be based around Ember.Object to be consistent with the rest of Ember, allowing you to manipulate your model via its get & set handlers. Alternatively, as demonstrated in the fiddle, you can use Em.Get/Em.set to access models, as long as you are consistent in doing so. If you touch your model directly (no get/set), then every won't be notified of your change.
Em.set(model.properties, 'foo', 'asdfsdf');
For completeness here's my every helper:
var DataValueObserver = Ember.Object.extend({
init: function() {
this._super();
// one way binding (for now)
Em.addObserver(this.parent, this.key, this, 'valueChanged');
},
value: function() {
return Em.get(this.parent, this.key);
}.property(),
valueChanged: function() {
this.notifyPropertyChange('value');
}
});
Handlebars.registerHelper("every", function() {
var args = [].slice.call(arguments);
var options = args.pop();
var context = (options.contexts && options.contexts[0]) || this;
Ember.assert("Must be in the form #every foo in bar ", 3 == args.length && args[1] === "in");
options.hash.keyword = args[0];
var property = args[2];
// if we're dealing with an array we can just forward onto the collection helper directly
var p = this.get(property);
if (Ember.Array.detect(p)) {
options.hash.dataSource = p;
return Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
}
// create an array that we will manage with content
var array = Em.ArrayController.create();
options.hash.dataSource = array;
Ember.Handlebars.helpers.collection.call(this, Ember.Handlebars.EachView, options);
//
var update_array = function(result) {
if (!result) {
array.clear();
return;
}
// check for proxy object
var result = (result.isProxy && result.content) ? result.content : result;
var items = result;
var keys = Ember.keys(items).sort();
// iterate through sorted array, inserting & removing any mismatches
var i = 0;
for ( ; i < keys.length; ++i) {
var key = keys[i];
var value = items[key];
while (true) {
var old_obj = array.objectAt(i);
if (old_obj) {
Ember.assert("Assume that all objects in our array have a key", undefined !== old_obj.key);
var c = key.localeCompare(old_obj.key);
if (0 === c) break; // already exists
if (c < 0) {
array.removeAt(i); // remove as no longer exists
continue;
}
}
// insert
if (typeof value === 'object') {
// wrap object so we can give it a key
value = Ember.ObjectProxy.create({
content: value,
isProxy: true,
key: key
});
array.insertAt(i, value);
} else {
// wrap raw value so we can give it a key and observe when it changes
value = DataValueObserver.create({
parent: result,
key: key,
});
array.insertAt(i, value);
}
break;
}
}
// remove any trailing items
while (array.objectAt(i)) array.removeAt(i);
};
var should_display = function() {
return true;
};
// use bind helper to call update_array if the contents of property changes
var child_properties = ["[]"];
var preserve_context = true;
return Ember.Handlebars.bind.call(context, property, options, preserve_context, should_display, update_array, child_properties);
});
Inspired by:
How can I make Ember.js handlebars #each iterate over objects?
http://mozmonkey.com/2014/03/ember-getting-the-index-in-each-loops/
https://github.com/emberjs/ember.js/issues/4365
https://gist.github.com/strathmeyer/1371586
Here's that fiddle again if you missed it:
http://jsfiddle.net/L6axcob8/1/
If I have a Backbone collection and want to create a copy of that collection with certain entries filtered out, how can I do that while keeping the copied instance as a Backbone.Collection?
Example:
var Module = Backbone.Model.extend();
var ModuleCollection = Backbone.Collection.extend({
model: Module
});
var modules = new ModuleCollection;
modules.add({foo: 'foo'},{foo: 'bar'});
console.log(modules instanceof Backbone.Collection); // true
var filtered = modules.filter(function(module) {
return module.get('foo') == 'bar';
});
console.log(filtered instanceof Backbone.Collection); // false
http://jsfiddle.net/m9eTY/
In the example above, I would like filtered to be a filtered version of modules, not just an array of models.
Essentially I would like to create a method in the collection instance that can filter out certain models and return the Backbone.Collection instance, but as soon as I start filtering the iteration methods returns an array.
You can wrap the filtered array in a temporary ModuleCollection if you want, the models filtered are the same instances of the ones in the original ModuleCollection, so if the module's attribute changes, it is still referenced by both collections.
so what I suggest you do is something like:
var filtered = new ModuleCollection(modules.filter(function (module) {
return module.get('foo') == 'bar';
}));
Since Backbone 0.9.2 there is an additional method called where that does the same:
var filtered = modules.where({foo: 'bar'});
that still returns an array though, so you will still need to wrap it as such:
var filtered = new ModuleCollection(modules.where({foo: 'bar'}));
For filtering collection using backbone
To make the filter you should have a filtered function in your collection
var MyCollection = Backbone.Collection.extend ({
filtered : function () {
I suggest to use UnderScore filter which will return true for valid and false for invalid where true is what you are looking for. use this.models to get the current collection models use model.get( '' ) to get the element you want to check for
var results = _.filter( this.models, function ( model ) {
if ( model.get('foo') == 'bar' )
return true ;
return false ;
});
Then use underscore map your results and transform it to JSON like this is probally where you are getting it wrong
results = _.map( results, function( model ) { return model.toJSON() } );
Finally returning a new backbone collection with only results this is how to make a copied collection
return new Backbone.Collection( results ) ;