Mongoose/MongoDB result fields appear undefined in Javascript - javascript

Is there something that I'm missing that would allow item to log as an object with a parameter, but when I try to access that parameter, it's undefined?
What I've tried so far:
console.log(item) => { title: "foo", content: "bar" } , that's fine
console.log(typeof item) => object
console.log(item.title) => "undefined"
I'll include some of the context just in case it's relevant to the problem.
var TextController = function(myCollection) {
this.myCollection = myCollection
}
TextController.prototype.list = function(req, res, next) {
this.myCollection.find({}).exec(function(err, doc) {
var set = new Set([])
doc.forEach(function(item) {
console.log(item) // Here item shows the parameter
console.log(item.title) // "undefined"
set.add(item.title)
})
res.json(set.get());
})
}
Based on suggestion I dropped debugger before this line to check what item actually is via the node repl debugger. This is what I found : http://hastebin.com/qatireweni.sm
From this I tried console.log(item._doc.title) and it works just fine.. So, this seems more like a mongoose question now than anything.
There are questions similar to this, but they seem to be related to 'this' accessing of objects or they're trying to get the object outside the scope of the function. In this case, I don't think I'm doing either of those, but inform me if I'm wrong. Thanks

Solution
You can call the toObject method in order to access the fields. For example:
var itemObject = item.toObject();
console.log(itemObject.title); // "foo"
Why
As you point out that the real fields are stored in the _doc field of the document.
But why console.log(item) => { title: "foo", content: "bar" }?
From the source code of mongoose(document.js), we can find that the toString method of Document call the toObject method. So console.log will show fields 'correctly'. The source code is shown below:
var inspect = require('util').inspect;
...
/**
* Helper for console.log
*
* #api public
*/
Document.prototype.inspect = function(options) {
var isPOJO = options &&
utils.getFunctionName(options.constructor) === 'Object';
var opts;
if (isPOJO) {
opts = options;
} else if (this.schema.options.toObject) {
opts = clone(this.schema.options.toObject);
} else {
opts = {};
}
opts.minimize = false;
opts.retainKeyOrder = true;
return this.toObject(opts);
};
/**
* Helper for console.log
*
* #api public
* #method toString
*/
Document.prototype.toString = function() {
return inspect(this.inspect());
};

Make sure that you have defined title in your schema:
var MyCollectionSchema = new mongoose.Schema({
_id: String,
title: String
});

Try performing a for in loop over item and see if you can access values.
for (var k in item) {
console.log(item[k]);
}
If it works, it would mean your keys have some non-printable characters or something like this.
From what you said in the comments, it looks like somehow item is an instance of a String primitive wrapper.
E.g.
var s = new String('test');
typeof s; //object
s instanceof String; //true
To verify this theory, try this:
eval('(' + item + ')').title;
It could also be that item is an object that has a toString method that displays what you see.
EDIT: To identify these issues quickly, you can use console.dir instead of console.log, since it display an interactive list of the object properties. You can also but a breakpoint and add a watch.

Use findOne() instead of find().
The find() method returns an array of values, even if you have only one possible result, you'll need to use item[0] to get it.
The findOne method returns one object or none, then you'll be able to access its properties with no issues.

Old question, but since I had a problem with this too, I'll answer it.
This probably happened because you're using find() instead of findOne(). So in the end, you're calling a method for an array of documents instead of a document, resulting in finding an array and not a single document. Using findOne() will let you get access the object normally.

A better way to tackle an issue like this is using doc.toObject() like this
doc.toObject({ getters: true })
other options include:
getters: apply all getters (path and virtual getters)
virtuals: apply virtual getters (can override getters option)
minimize: remove empty objects (defaults to true)
transform: a transform function to apply to the resulting document before returning
depopulate: depopulate any populated paths, replacing them with their original refs (defaults to false)
versionKey: whether to include the version key (defaults to true)
so for example you can say
Model.findOne().exec((err, doc) => {
if (!err) {
doc.toObject({ getters: true })
console.log('doc _id:', doc._id) // or title
}
})
and now it will work

You don't have whitespace or funny characters in ' title', do you? They can be defined if you've quoted identifiers into the object/map definition. For example:
var problem = {
' title': 'Foo',
'content': 'Bar'
};
That might cause console.log(item) to display similar to what you're expecting, but cause your undefined problem when you access the title property without it's preceding space.

I think using 'find' method returns an array of Documents.I tried this and I was able to print the title
for (var i = 0; i < doc.length; i++) {
console.log("iteration " + i);
console.log('ID:' + docs[i]._id);
console.log(docs[i].title);
}

If you only want to get the info without all mongoose benefits, save i.e., you can use .lean() in your query. It will get your info quicker and you'll can use it as an object directly.
https://mongoosejs.com/docs/api.html#query_Query-lean
As says in docs, this is the best to read-only scenarios.

Are you initializing your object?
function MyObject()
{
this.Title = "";
this.Content = "";
}
var myo1 = new MyObject();
If you do not initialize or have not set a title. You will get undefined.

When you make tue query, use .lean() E.g
const order = await Order.findId("84578437").lean()

find returns an array of object , so to access element use indexing, like
doc[0].title

Related

How to get returned values from my listener's callback ES6 way

I made an input that let me filter a table of softwares.
<input type="text" id="softwares-search" class="form-control" aria-label="Input de recherche" aria-describedby="softwares-search">
Then in javascript my filter work well if I console.log(....)
But when I replace it with a return, nothing is returned. I think it is due to my var affectation through the event listener :
const maxwell = () => {
search = document.querySelector('#softwares-search').value;
return softwares.filter(row => row.name.includes(search) || row.description.includes(search));
}
const softwaresSearch = document.querySelector('#softwares-search');
if (softwaresSearch) {
var results = softwaresSearch.addEventListener('keyup', maxwell)
console.log(results);
}
Thank all
EDIT 1 :
I was so angry, so blind, I had S#!t in my eyes, no need to use a global :(
const softwaresSearch = document.getElementById('softwares-search');
if (softwaresSearch) {
softwaresSearch.addEventListener('keyup', (e) => {
search = document.getElementById('softwares-search').value;
var filtredSoftwares = softwares.filter(e => e.name.includes(search) || e.description.includes(search) );
renderTable(filtredSoftwares);
});
}
const renderTable = (softwares) => {
Object.values(softwares).forEach(value=>{
console.log(value);
});
// Todo build HTML table
}
Instead of returning I think you just need to replace the current array like this
const maxwell = () => {
search = document.querySelector('#softwares-search').value;
softwares = softwares.filter(row => row.name.includes(search) || row.description.includes(search));
}
And results is not needed:
const softwaresSearch = document.querySelector('#softwares-search');
if (softwaresSearch) {
softwaresSearch.addEventListener('keyup', maxwell)
}
As far as I know, softwareSearch.addEventListener won't return anything, since that is an event listener, and does not return any value. It simply executes the function passed in the 2nd parameter. You could try doing this instead
softwaresSearch.addEventListener('keyup', () => {
var results = maxwell();
console.log(results);
});
What this would do is that, it would call your maxwell function when the keyup event, since that is what it looks you are trying to do.
Please share all relevant code before posting a question, this code includes the variable "softwares" that exist outside what is visible to us.
Additionally, there are some issues with your code.
I don't understand your naming of the maxwell function. You should name functions as verbs, not anything else. A function is a machine that is doing something, and possibly returning something. It should be named to what it is doing.
On the second line, you say "search = ... ", but you didn't declare it as a variable.
You are returning something based on a value that isn't validated ('search' can be either undefined or a string value in this case), hence, your return will most likely just return undefined and not any value at all.
Your function can possibly not return anything at all since you are returning something within your if-statement. You can use a closure to always return something.
I would also suggest passing a search string as a variable to your function that should return a list based on the search query. Getting in the habit of short, concise functions with expected inputs/outputs, will make your code more readable and less error-prone and less likely to produce unwanted side-effects.
I don't know the rest of your code, but I don't recommend assigning variables in the global scope. Your "maxwell", "softwareSearch" variables both exist in the global space, unless you have wrapped them in another function block already (such as jquerys $(document).ready(() => { ...everything here is scoped })
You are getting the same element in two different places in your code.
Here is an updated code sample, but I can't test it since I don't know the rest of your code.
/*
* Executing the whole thing in this IIFE will make all variables declared inside here scoped to this block only,
* thus they can't interfere with other code you may write
*/
(() => {
const listOfSoftwares = softwares; // --- get your softwares variable here somehow, I don't know where "software" comes from.
// input element
const search = document.querySelector('#softwares-search');
/**
* Filter search results
* #param {string} query Search query
* #returns {Array} The results array
*/
const filterSoftwareSearchResults = (query) => {
let results = [];
results = listOfSoftwares.filter(software => software.description.includes(query) || software.title.includes(query))
// Verify
console.log(results);
// Should return array of results, even if empty
return results;
}
if (search) {
search.addEventListener('keyup', () => {
filterSoftwareSearchResults(search.value)
})
}
})()
The addEventListener function always returns undefined, so your results variable is undefined.
Returning from the callback function (maxwell) is also of no use.
You either need to do something with the data inside of your callback, or maybe pass the data to a global variable.

How to include or detect the name of a new Object when it's created from a Constructor

I have a constructor that include a debug/log code and also a self destruct method
I tried to find info on internet about how to detect the new objects names in the process of creation, but the only recommendation that I found was pass the name as a property.
for example
var counter = {}
counter.a =new TimerFlex({debug: true, timerId:'counter.a'});
I found unnecessary to pass counter.a as a timerId:'counter.a' there should be a native way to detect the name from the Constructor or from the new object instance.
I am looking for something like ObjectProperties('name') that returns counter.a so I don't need to include it manually as a property.
Adding more info
#CertainPerformance What I need is to differentiate different objects running in parallel or nested, so I can see in the console.
counter.a data...
counter.b data...
counter.a data...
counter.c data... etc
also these objects have only a unique name, no reference as counter.a = counter.c
Another feature or TimerFlex is a method to self desruct
this.purgeCount = function(manualId) {
if (!this.timerId && manualId) {
this.timerId = manualId;
this.txtId = manualId;
}
if (this.timerId) {
clearTimeout(this.t);
this.timer_is_on = 0;
setTimeout ( ()=> { console.log(this.txtId + " Destructed" ) },500);
setTimeout ( this.timerId +".__proto__ = null", 1000);
setTimeout ( this.timerId +" = null",1100);
setTimeout ( "delete " + this.timerId, 1200);
} else {
if (this.debug) console.log("timerId is undefined, unable to purge automatically");
}
}
While I don't have a demo yet of this Constructor this is related to my previous question How to have the same Javascript Self Invoking Function Pattern running more that one time in paralel without overwriting values?
Objects don't have names - but constructors!
Javascript objects are memory references when accessed via a variables. The object is created in the memory and any number of variables can point to that address.
Look at the following example
var anObjectReference = new Object();
anObjectReference.name = 'My Object'
var anotherReference = anObjectReference;
console.log(anotherReference.name); //Expected output "My Object"
In this above scenario, it is illogical for the object to return anObjectReference or anotherReference when called the hypothetical method which would return the variable name.
Which one.... really?
In this context, if you want to condition the method execution based on the variable which accesses the object, have an argument passed to indicate the variable (or the scenario) to a method you call.
In JavaScript, you can access an object instance's properties through the same notation as a dictionary. For example: counter['a'].
If your intent is to use counter.a within your new TimerFlex instance, why not just pass counter?
counter.a = new TimerFlex({debug: true, timerId: counter});
// Somewhere within the logic of TimerFlex...
// var a = counter.a;
This is definitely possible but is a bit ugly for obvious reasons. Needless to say, you must try to avoid such code.
However, I think this can have some application in debugging. My solution makes use of the ability to get the line number for a code using Error object and then reading the source file to get the identifier.
let fs = require('fs');
class Foo {
constructor(bar, lineAndFile) {
this.bar = bar;
this.lineAndFile = lineAndFile;
}
toString() {
return `${this.bar} ${this.lineAndFile}`
}
}
let foo = new Foo(5, getLineAndFile());
console.log(foo.toString()); // 5 /Users/XXX/XXX/temp.js:11:22
readIdentifierFromFile(foo.lineAndFile); // let foo
function getErrorObject(){
try { throw Error('') } catch(err) { return err; }
}
function getLineAndFile() {
let err = getErrorObject();
let callerLine = err.stack.split("\n")[4];
let index = callerLine.indexOf("(");
return callerLine.slice(index+1, callerLine.length-1);
}
function readIdentifierFromFile(lineAndFile) {
let file = lineAndFile.split(':')[0];
let line = lineAndFile.split(':')[1];
fs.readFile(file, 'utf-8', (err, data) => {
if (err) throw err;
console.log(data.split('\n')[parseInt(line)-1].split('=')[0].trim());
})
}
If you want to store the variable name with the Object reference, you can read the file synchronously once and then parse it to get the identifier from the required line number whenever required.

checking thruthness of overwritten valueOf not working

I want to create a similar construction in my code.
var inList = findItem(list, data);
if(!inList) {
var item = inList.item;
}
function findItem(list, data) {
var item = list.find("[data-day='"+data.day+"']")
// more code.
// conditional return
return {item: item, valueOf:function(){return false}};
}
But it doesn't work because overwriting valueOf doesn't play nicely with a simple truthfull check (in the way that I want it to work).
and having code like if(inList == false){} looks less clean imo. Is there a way to make this work?
Boolean checks don't invoke valueOf - all objects are considered truthy. If you want to circumvent that, you'll have to invoke it yourself explicitly:
if (!inList.valueOf()) …
You should not depend on code that uses valueOf,
if you wanted to do something where you are returning an object,
just add another property instead.
var findResult = findItem(list, data);
if(!findResult.found) {
var item = findResult.item;
}
function findItem(list, data) {
var item = list.find("[data-day='"+data.day+"']");
// more code.
// conditional return
return {item: item, found: false};
}
Then again, I forgot what I was doing 5 years ago.

How can I make Ember.js handlebars #each iterate over objects?

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/

How to make Backbone.js Collection items Unique?

Say I have these Backbone.js Model:
var Truck = Backbone.Model.extend({});
var truck1 = new Truck();
var truck2 = new Truck();
truck1.set("brand", "Ford");
truck2.set("brand", "Toyota");
truck3.set("brand", "Honda");
truck4.set("brand", "Ford");
Then, let's say we have a Backbone.js Collection:
var TruckList = Backbone.Collection.extend({
model: Truck,
comparator: function(truck) {
return truck.get("brand");
};
});
I'm a car collector, so time to add each car to my collection:
Trucks = new TruckList();
Trucks.add(truck1);
Trucks.add(truck2);
Trucks.add(truck3);
Trucks.add(truck4);
Just focusing on the brand attribute, truck4 is a duplicate of truck1. I can't have duplicates in my Collection. I need my collection to have unique values.
My question is, How do I remove duplicate items from my Backbone.js Collection?
Should I use Underscore.js for this? If so, can someone please provide a working/runnable example of how to do this.
Assume the following:
1.Collection is not sorted
Removal must be done on brand attribute value
Ajax call to populate each instance of a Truck. This means when adding to a collection, you don't have access to the Truck properties.
I would override the add method in your TruckList collection and use underscore to detect duplicates there and reject the duplicate. Something like.
TruckList.prototype.add = function(truck) {
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
// Up to you either return false or throw an exception or silently ignore
// NOTE: DEFAULT functionality of adding duplicate to collection is to IGNORE and RETURN. Returning false here is unexpected. ALSO, this doesn't support the merge: true flag.
// Return result of prototype.add to ensure default functionality of .add is maintained.
return isDupe ? false : Backbone.Collection.prototype.add.call(this, truck);
}
The simplest way to achieve this is to make sure the models you are adding have unique ids. By default Backbone collections will not add models with duplicate ids.
test('Collection should not add duplicate models', 1, function() {
var model1 = {
id: "1234"
};
var model2 = {
id: "1234"
};
this.collection.add([model1, model2]);
equal(1, this.collection.length, "collection length should be one when trying to add two duplicate models");
});
Try this. It uses the any underscore method to detect the potential duplicate and then dumps out if so. Of course, you might want to dress this up with an exception to be more robust:
TruckList.prototype.add = function(newTruck) {
var isDupe = this.any(function(truck) {
return truck.get('brand') === newTruck.get('brand');
}
if (isDupe) return;
Backbone.Collection.prototype.add.call(this, truck);
}
As an aside, I would probably write a function on Truck to do the dupe checking so that the collection doesn't know too much about this condition.
var TruckList = Backbone.Collection.extend({
model : Truck,
// Using #Peter Lyons' answer
add : function(truck) {
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
if (isDupe) {
// Up to you either return false or throw an exception or silently
// ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
},
comparator : function(truck) {
return truck.get("brand");
} });
VassilisB's answer worked great but it will override Backbone Collection's add() behavior. Therefore, errors might come when you try to do this:
var truckList = new TruckList([{brand: 'Ford'}, {brand: 'Toyota'}]);
So, I added a bit of a checking to avoid these errors:
var TruckList = Backbone.Collection.extend({
model : Truck,
// Using #Peter Lyons' answer
add : function(trucks) {
// For array
trucks = _.isArray(trucks) ? trucks.slice() : [trucks]; //From backbone code itself
for (i = 0, length = trucks.length; i < length; i++) {
var truck = ((trucks[i] instanceof this.model) ? trucks[i] : new this.model(trucks[i] )); // Create a model if it's a JS object
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
if (isDupe) {
// Up to you either return false or throw an exception or silently
// ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
}
},
comparator : function(truck) {
return truck.get("brand");
}});
I'm doing a FileUpload thing with the same issue, and here's how I did it (coffeescript):
File = Backbone.Model.extend
validate: (args) ->
result
if !#collection.isUniqueFile(args)
result = 'File already in list'
result
Files = Backbone.Collection.extend
model: File
isUniqueFile: (file) ->
found
for f in #models
if f.get('name') is file.name
found = f
break
if found
false
else
true
... and that's it. The collection object is automatically referenced in File, and Validation is automatically called when an action is invoked on the collection which in this case is Add.
Underscore.js, a pre-req for backbone.js, provides a function for this: http://documentcloud.github.com/underscore/#uniq
Example:
_.uniq([1,1,1,1,1,2,3,4,5]); // returns [1,2,3,4,5]
Not sure if this is an update to either Backbone or underscore, but the where() function works in Backbone 0.9.2 to do the matching for you:
TruckList.prototype.add = function(truck) {
var matches = this.where({name: truck.get('brand')});
if (matches.length > 0) {
//Up to you either return false or throw an exception or silently ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
}
I would prefer override the add method like this.
var TruckList = Backbone.Collection.extend({
model : Truck,
// Using #Peter Lyons' answer
add : function(truck) {
// Using isDupe routine from #Bill Eisenhauer's answer
var isDupe = this.any(function(_truck) {
return _truck.get('brand') === truck.get('brand');
});
if (isDupe) {
// Up to you either return false or throw an exception or silently
// ignore
return false;
}
Backbone.Collection.prototype.add.call(this, truck);
},
comparator : function(truck) {
return truck.get("brand");
} });
It seems like an elegant solution would be to use _.findWhere so long as you have some unique attribute (brand in your case). _.findWhere will return a match which is a JavaScript object and therefore truthy or undefined which is falsey. This way you can use a single if statement.
var TruckList = Backbone.Collection.extend({
model: Truck,
add: function (truck) {
if (!this.findWhere({ brand: truck.get('brand') })) {
Backbone.Collection.prototype.add.call(this, truck);
}
}
});
Try this...
var TruckList = Backbone.Collection.extend({
model: Truck,
comparator: function(truck) {
return truck.get("brand");
},
wherePartialUnique: function(attrs) {
// this method is really only tolerant of string values. you can't do partial
// matches on arrays, objects, etc. use collection.where for that
if (_.isEmpty(attrs)) return [];
var seen = [];
return this.filter(function(model) {
for (var key in attrs) {
// sometimes keys are empty. that's bad, so let's not include it in a unique result set
// you might want empty keys though, so comment the next line out if you do.
if ( _.isEmpty(model.get(key).trim()) ) return false;
// on to the filtering...
if (model.get(key).toLowerCase().indexOf(attrs[key].toLowerCase()) >= 0) {
if (seen.indexOf( model.get(key) ) >= 0 ) return false;
seen.push(model.get(key));
return true;
} else {
return false;
}
}
return true;
});
}
});
A few things to remember:
this is based on the backbone.collection.where method and unlike that method, it will attempt partial matches on model attributes within a collection. If you don't want that, you'll need to modify it to only match exactly. Just mimic what you see in the original method.
it should be able to accept multiple attribute matches, so if you have model attributes of foo and bar, you should be able to do collection.wherePartialUnique({foo:"you",bar:"dude"}). I have not tested that though. :) I have only ever done one key/value pair.
i also strip out empty model attributes from consideration. I don't care about them, but you might.
this method doesn't require a collection of unique model properties that the comparator depends. It's more like a sql distinct query, but I'm not an sql guy so don't shoot me if that's a bad example :)
your collection is sorted by way of the comparator function, so one of your assumptions about it not being sorted is incorrect.
I believe this also addresses all of your goals:
Collection is not sorted
Removal must be done on brand attribute value
Ajax call to populate each instance of a Truck. This means when adding to a collection, you don't have access to the Truck properties.
I'm really unhappy with the accepted answer to this solution. It contains numerous errors. I've edited the original solution to highlight my concerns, but I am proposing the following solution assuming you're OK dirtying your duplicate's id/cid property:
TruckList.prototype.add = function(truckToAdd, options) {
// Find duplicate truck by brand:
var duplicateTruck = this.find(function(truck){
return truck.get('brand') === truckToAdd.get('brand');
});
// Make truck an actual duplicate by ID:
// TODO: This modifies truckToAdd's ID. This could be expanded to preserve the ID while also taking into consideration any merge: true options.
if(duplicateTruck !== undefined){
if(duplicateTruck.has('id')){
truckToAdd.set('id', duplicateTruck.get('id'), { silent: true });
}
else {
truckToAdd.cid = duplicateTruck.cid;
}
}
// Allow Backbone to handle the duplicate instead of trying to do it manually.
return Backbone.Collection.prototype.add.call(this, truckToAdd, options);
}
The only flaw with this one is that truckToAdd's ID/cid is not preserved. However, this does preserve all of the expected functionality of adding an item to a collection including passing merge: true.
I was not satisfied with the provided answers for several reasons:
Modifying the return value of add is unexpected.
Not supporting { merge: true } is unexpected.
I've provided a solution which I believe to be more robust. This solution clones given models if they have duplicates in the collection, updates the clones' ID to match the duplicates ID, and then passes the list of duplicates and non-duplicates onto the original add method so that it can do its magic. No unintended side-effects as far as I am aware.
add: function (models, options) {
var preparedModels;
if (models instanceof Backbone.Collection) {
preparedModels = models.map(this._prepareModelToAdd.bind(this));
}
else if (_.isArray(models)) {
preparedModels = _.map(models, this._prepareModelToAdd.bind(this));
} else if (!_.isNull(models) && !_.isUndefined(models)) {
preparedModels = this._prepareModelToAdd(models);
} else {
preparedModels = models;
}
// Call the original add method using preparedModels which have updated their IDs to match any existing models.
return Backbone.Collection.prototype.add.call(this, preparedModels, options);
},
// Return a copy of the given model's attributes with the id or cid updated to match any pre-existing model.
// If no existing model is found then this function is a no-op.
// NOTE: _prepareModel is reserved by Backbone and should be avoided.
_prepareModelToAdd: function (model) {
// If an existing model was not found then just use the given reference.
var preparedModel = model;
var existingModel = this._getExistingModel(model);
// If an existing model was found then clone the given reference and update its id.
if (!_.isUndefined(existingModel)) {
preparedModel = this._clone(model);
this._copyId(preparedModel, existingModel);
}
return preparedModel;
},
// Try to find an existing model in the collection based on the given model's brand.
_getExistingModel: function (model) {
var brand = model instanceof Backbone.Model ? model.get('brand') : model.brand;
var existingModel = this._getByBrand(brand);
return existingModel;
},
_getByBrand: function (brand) {
return this.find(function (model) {
return model.get('brand') === brand;
});
},
_clone: function (model) {
// Avoid calling model.clone because re-initializing the model could cause side-effects.
// Avoid calling model.toJSON because the method may have been overidden.
return model instanceof Backbone.Model ? _.clone(model.attributes) : _.clone(model);
},
// Copy the model's id or cid onto attributes to ensure Backbone.Collection.prototype.add treats attributes as a duplicate.
_copyId: function (attributes, model) {
if (model.has('id')) {
attributes.id = model.get('id');
} else {
attributes.cid = model.cid;
}
}

Categories

Resources