I have some search functionality that I am working on, every time a user types into a text input I filter a collection, here is the code,
userSearch: function() {
var that = this;
var letters = $('.js-user-search').val();
this.filteredCollection.reset(that.filterUsers( that.collection, letters));
var resultsList = new app.SearchUserResults({
collection: this.filteredCollection
});
resultsList.render();
},
filterUsers: function( collection, filterValue) {
var filteredCollection;
if (filterValue === "") {
return collection.toJSON();
}
return filteredCollection = collection.filter(function(data) {
return _.some(_.values(data.toJSON()), function(value) {
if( value != undefined ) {
value = (!isNaN(value) ? value.toString() : value);
//var re = /^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)|(\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return value.indexOf(filterValue) >= 0;
}
});
});
}
As you can see from the code above, I pass a collection (users) and the search parameters to filterUsers(), that then returns a collection of matching models. I am then trying to render that into a list of search results ( links ), but the events on those links run several times (dependent on the length of the search string).
How can I build a list of results from the return collection? I have tried adding,
this.filteredCollection.on('reset', this.doSomething); however this never seems to get run, I have tried initialising my results view in the initialise function also, but I cannot pass the collection to that view as it is empty what is the best way to go?
you have to be careful with views in backbone. You keep adding a new searchresults view without removing the old one. Always keep a reference to views you add multiple times so that you can remove the previous one. I think this part will help you out:
var myCurrentSearchList = null;
userSearch: function() {
var that = this;
var letters = $('.js-user-search').val();
this.filteredCollection.reset(that.filterUsers( that.collection, letters));
if (myCurrentSearchList) {
myCurrentSearchList.remove();
}
var resultsList = new app.SearchUserResults({
collection: this.filteredCollection
});
myCurrentSearchList = resultsList;
resultsList.render();
},
http://backbonejs.org/#View-remove
Related
I'm trying to get either options or, ideally, dynamicTable passed from initializeTable to the applyTableFilters function and I'm having problems getting the expected values. I'm using List.js to make a table dynamic and I need to pass or recreate the dynamicTable object so I can go ahead and use it to filter the table.
Here is the function that creates the List.js object from the HTML table:
function initializeTable(options) { // initializes table to be dynamic using List.js functions
var dynamicTable = new List("table-content", options);
dynamicTable.on("updated", function (list) { // writes a message to the user if no results are found
if (list.matchingItems.length == 0) {
document.getElementById("no-results").style.display = "block";
}
else {
document.getElementById("no-results").style.display = "none";
}
});
console.log(dynamicTable);
console.log(options);
console.log(arguments.length);
applyTableFilters.bind();
}
I've tried different methods to pass the variables to the function below. I tried .call, applyTableFilters(args), and .apply, but the problem is that I do not want the function to execute from inside here, only when the click event from the button goes off (not shown in these functions).
This is the function I want to pass the object to and proceed to make the filter functions using it:
function applyTableFilters(dynamicTable) {
var form = document.getElementById("filter-form");
//console.log(options);
//var dynamicTable = new List("table-content", options);
console.log(dynamicTable);
var filters = form.querySelectorAll('input[type="checkbox"]:checked');
dynamicTable.filter(function (item) {
console.log(item);
console.log(item._values);
if (item.values().id == 2) {
return true;
}
else {
return false;
}
//var filterStrings = [];
//console.log(filters);
//for (var i = 0; i < filters.length; i++) {
// var filterVal = filters[i].value;
// var filterString = "(" + item.values().column == filterVal + ")"; // filterVal.contains(item.values().column) ||
// filterStrings.push(filterString);
// console.log(filterVal);
// console.log(filterString);
//}
//console.log(filterStrings);
//var filterString = filterStrings.join(" && ");
//console.log(filterString);
//return filterString;
});
}
I've used:
applyTableFilters.bind(this, dynamicTable/options);
applyTableFilters.bind(null, dynamicTable/options);
applyTableFilters.bind(dynamicTable/options);
Switching between the two since I don't need both passed if one ends up working, etc. I always get a mouse event passed in and that's not even the right type of object I'm looking for. How can I get the right object passed? Also all the values in the first function are not empty and are populated as expected so it's not the original variables being undefined or null. Thanks in advance.
From your initializeTable function return a function that wraps the applyTableFilters function with the arguments you want.
Then assign the returned function to a var to be executed later.
function initializeTable(options) {
var dynamicTable = new List("table-content", options);
// other stuff
return function () {
applyTableFilters(dynamicTable)
}
}
// other stuff
var applyTableFiltersPrep = initializeTable(options)
// later, when you want to execute...
applyTableFiltersPrep()
JSFiddle example
The Setup
I am new to Backbone, and am using it with Backgrid to display a large amount of data. The data represents two lists of Ingredients: one with existing and one with updated values. There are no primary keys for this data in the DB so the goal is to be able to match the old and new Ingredients manually by name and then generate a DB update from the matched data. To do this I have three collections: ingredientsOld (database), ingredientsNew (update.xml), and ingredients. The ingredientsOld and ingredientsNew collections are just collections of the basic Ingredient model. The ingredients collection, however, is a collection of IngredientComp models which contain an integer status, an 'old' Ingredient, and a 'new' Ingredient.
var Ingredient = Backbone.Model.extend({});
var Ingredients = Backbone.Collection.extend({ model: Ingredient });
var IngredientComp = Backbone.Model.extend({
constructor: function(attributes, options) {
Backbone.Model.apply( this, arguments );
if (attributes.o instanceof Ingredient) {
this.o = attributes.o;
console.log("Adding existing ingredient: "+this.o.cid);
} else {
this.o = new Ingredient(attributes.o);
console.log("Adding new ingredient: "+this.o.get("name"));
}
if (attributes.n instanceof Ingredient) {
this.n = attributes.n;
} else {
this.n = new Ingredient(attributes.n);
}
}
});
var IngredientComps = Backbone.Collection.extend({
model: IngredientComp,
comparator: function(comp){
return -comp.get("status");
}
});
var ingredientsOld = new Ingredients();
var ingredientsNew = new Ingredients();
var ingredients = new IngredientComps();
The data is being generated by PHP and outputted to JSON like so:
ingredientsOld.add([
{"name":"Milk, whole, 3.25%","guid":"3BDA78C1-69C1-4582-83F8-5A9D00E58B45","item_id":16554,"age":"old","cals":"37","fat_cals":"18","protein":"2","carbs":"3","fiber":"0","sugar":"3","fat":"2","sat_fat":"1","trans_fat":"0","chol":"6","sod":"24","weight":"2.00","quantity":"1 each","parents":{"CC09EB05-4827-416E-995A-EBD62F0D0B4A":"Baileys Irish Cream Shake"}}, ...
ingredients.add([
{"status":3,"o":{"name":"Sliced Frozen Strawberries","guid":"A063D161-A876-4036-ADB0-C5C35BD9E5D5","item_id":16538,"age":"old","cals":"77","fat_cals":"0","protein":"1","carbs":"19","fiber":"1","sugar":"19","fat":"0","sat_fat":"0","trans_fat":"0","chol":"0","sod":"0","weight":"69.60","quantity":"1 each","parents":{"BC262BEE-CED5-4AB3-A207-D1A04E5BF5C7":"Lemonade"}},"n":{"name":"Frozen Strawberries","guid":"5090A352-74B4-42DB-8206-3FD7A7CF9D56","item_id":"","age":"new","cals":"77","fat_cals":"0","protein":"1","carbs":"19","fiber":"1","sugar":"19","fat":"0","sat_fat":"0","trans_fat":"0","chol":"0","sod":"0","weight":"","quantity":"69.60 Gram","parents":{"237D1B3D-7871-4C05-A788-38C0AAC04A71":"Malt, Strawberry"}}}, ...
When I display values from the IngredientComp model (from the render function of a custom Backgrid Cell), I initially have to output them like this:
render: function() {
col = this.column.get("name");
var v1 = this.model.get("o")[col];
var v2 = this.model.get("n")[col];
this.$el.html( v1 + "\n<br />\n<b>" + v2 + "</b>" );
return this;
}
The Problem
It is only after moving the IngredientComp models from one collection to another that the this.model.get("o").get(col); function works. Here is the function that moves the Ingredients from one collection to another:
function matchItems(oldId, newId) {
var oldItem = ingredientsOld.remove(oldId);
var newItem = ingredientsNew.remove(newId);
ingredients.add({'status': 1, 'o': oldItem, 'n': newItem});
}
I have updated the render function to try both methods of retrieving the value, but it is a bit slower and certainly not the proper way of handling the problem:
render: function() {
col = this.column.get("name");
// Investigate why the Ingredient model's get() method isn't available initially
var v1 = this.model.get("o")[col];
// The above line returns 'undefined' if the Ingredient model has moved from one
// collection to another, so we have to do this:
if (typeof v1 === "undefined"){ v1 = this.model.get("o").get(col)};
var v2 = this.model.get("n")[col];
if (typeof v2 === "undefined"){ v2 = this.model.get("n").get(col)};
this.$el.html( v1 + "\n<br />\n<b>" + v2 + "</b>" );
return this;
}
Can anyone shed some light on what might be causing this problem? I have done a bit of research on Backbone-relational.js, but it seems like a lot of overkill for what I am trying to accomplish.
I would first recommend using initialize instead of constructor, because the constructor function overrides and delays the creation of the model.
The main issue thought is that model.get('o') returns something different in this if statement. by doing this.o it is not setting the attribute on the model, but instead setting it on the model object. Therefore when the model is actually created model.get('o') is a regular object and not a backbone model.
if (attributes.o instanceof Ingredient) {
this.o = attributes.o;
console.log("Adding existing ingredient: "+this.o.cid);
} else {
this.o = new Ingredient(attributes.o);
console.log("Adding new ingredient: "+this.o.get("name"));
}
Changing the if statement to the following should solve the issue.
if (attributes.o instanceof Ingredient) {
this.o = attributes.o;
console.log("Adding existing ingredient: "+this.o.cid);
} else {
this.set('0', new Ingredient(attributes.o));
console.log("Adding new ingredient: "+this.o.get("name"));
}
I am quite new to knockout.js, and I am enjoying learning how to make interfaces with it. But I have a bit of a wall while trying to make my interface more efficient. What I am trying to achieve is remove only the elements selected by $('.document_checkbox').serializeArray(), which contains the revision_id. I will then re-add the entries to the view model with a modified call to self.getDocument(), passing only the modified records which will be re-added. Can anyone help me how to remove the entries from the arrays based on the 'revision_id' values of $('.document_checkbox').serializeArray()
?
function Document(data) {
this.line_id = data.line_id
this.revision_id = ko.observable(data.revision_id);
this.status_id = ko.observable(data.status_id);
}
function DocumentViewModel() {
var self = this;
self.documents = ko.observableArray([]);
self.getDocument = function(){
//Reset arrays
self.documents.removeAll();
//Dynamically build section arrays
$.getJSON("/Documentation/Get-Section", function(allData) {
$.map(allData, function(item) {
var section = { name: item.array_name, display_name: item.display_name, documents: ko.observableArray([])};
self.documents.push(section);
})
//Add document objects to the arrays
$.getJSON("/Documentation/Get-Document", function(allData){
$.map(allData, function(item) {
var section = ko.utils.arrayFirst(self.documents(), function(documentSection) {
return documentSection.name === item.array_name;
});
section.documents.push(new Document(item));
});
});
});
}
self.updateStatusBatch = function(data,event){
$.post('/Documentation/Update-Status-Batch',
{
revision_id : $('.document_checkbox').serializeArray(),
status_id : event.currentTarget.value
}).done(
function(){
//This is where I get confused.
});
}
}
You should modify the /Documentation/Update-Status-Batch in order that it returns the deleted item id. So you will be able to remove it on the client side.
Try this "done" function:
function(removedItemId) {
self.documents.remove(function(doc){
return doc.status_id == removedItemId;
})
}
Take a look at the remove function.
I hope it helps.
I'm using KnockoutJS with the Knockout-Validation plugin to validate fields on a form. I'm having problems validating that a value is unique using the native validation rule - unique
I'm using the Editor Pattern from Ryan Niemeyer to allow the user to edit or create a Location. Here's my fiddle to see my problem in its entirety.
function Location(data, names) {
var self = this;
self.id = data.id;
self.name = ko.observable().extend({ unique: { collection: names }});
// other properties
self.errors = ko.validation.group(self);
// update method left out for brevity
}
function ViewModel() {
var self = this;
self.locations = ko.observableArray([]);
self.selectedLocation = ko.observable();
self.selectedLocationForEditing = ko.observable();
self.names = ko.computed(function(){
return ko.utils.arrayMap(self.locations(), function(item) {
return item.name();
});
});
self.edit = function(item) {
self.selectedLocation(item);
self.selectedLocationForEditing(new Location(ko.toJS(item), self.types));
};
self.cancel = function() {
self.selectedLocation(null);
self.selectedLocationForEditing(null);
};
self.update = function(item) {
var selected = self.selectedLocation(),
updated = ko.toJS(self.selectedLocationForEditing()); //get a clean copy
if(item.errors().length == 0) {
selected.update(updated);
self.cancel();
}
else
alert("Error");
};
self.locations(ko.utils.arrayMap(seedData, function(item) {
return new Location(item, self.types, self.names());
}));
}
I'm having an issue though. Since the Location being edited is "detached" from the locations observableArray (see Location.edit method), when I make changes to name in the detached Location that value isn't updated in the names computed array. So when the validation rule compares it to the names array it will always return a valid state of true since the counter will only ever be 1 or 0. (Please see knockout-validation algorithm below)
Within the options argument for the unique validation rule I can pass in a property for externalValue. If this value is not undefined then it will check to see if the count of matched names is greater or equal to 1 instead of 2. This works except for cases when the user changes the name, goes on to another field, and then goes back to the name and wants to change it back to the original value. The rule just sees that the value already exists in the names array and returns a valid state of false.
Here is the algorithm from knockout.validation.js that handles the unique rule...
function (val, options) {
var c = utils.getValue(options.collection),
external = utils.getValue(options.externalValue),
counter = 0;
if (!val || !c) { return true; }
ko.utils.arrayFilter(ko.utils.unwrapObservable(c), function (item) {
if (val === (options.valueAccessor ? options.valueAccessor(item) : item)) { counter++; }
});
// if value is external even 1 same value in collection means the value is not unique
return counter < (external !== undefined && val !== external ? 1 : 2);
}
I've thought about using this as a base to create a custom validation rule but I keep getting stuck on how to handle the situation when the user wants go back to the original value.
I appreciate any and all help.
One possible solution is to not include the name of the currently edit item (of course when creating a new item you need the full list) in the unique validator.
So the unique check won't be triggered when changing the Location name back to its original value:
self.namesExceptCurrent = function(name){
return ko.utils.arrayMap(self.locations(), function(item) {
if (item.name() !== name)
return item.name();
});
}
self.edit = function(item) {
self.selectedLocation(item);
self.selectedLocationForEditing(
new Location(ko.toJS(item),
self.types,
self.namesExceptCurrent(item.name())));
};
Demo JSFiddle.
Mates
I have the following:
App.Collections.Bookings = Backbone.Collection.extend({
url: 'bookings/',
model: App.Models.Booking,
howManyArriving: function() {
var bg = _.countBy( this.models, function(model) {
return model.get('indate') == moment().format('YYYY-MM-DD') ? 'even' : 'odd';
});
var lv = _.filter( this.models, function(model){
return model.get('indate') == moment().format('YYYY-MM-DD');
});
var r = {
count: bg,
models: lv
}
return r;
},
availableBtwn: function(bed,indate,outdate) {
var gf = _.filter(this.models, function(model){
return (
model.get('outdate') > outdate &&
model.get('indate') <= indate &&
model.get('id_bed') == bed
);
});
return gf;
},
getBooking: function(bed, date){
var gf = _.filter(this.models, function(model){
return (
model.get('outdate') > date &&
model.get('indate') <= date &&
model.get('id_bed') == bed
);
});
return gf;
},
getFullName: function(id){
var b = this.get(id);
return b.get('nombre') + ' ' + b.get('apellido');
}
});
I need to check when I populate the collection and when I add a single model if there's already an existing model with determined propperties equal to the model/s that i'm attempting to create.
I've tried something like this:
App.Collections.Bookings.prototype.add = function(bookings) {
_.each( bookings, function(book){
var isDupe = this.any(function(_book) {
return _book.get('id') === book.id;
});
if (isDupe) {
//Up to you either return false or throw an exception or silently ignore
return false;
}else{
Backbone.Collection.prototype.add.call(this, book);
}
//console.log('Cargo el guest: ' + guest.get('id'));
}, this);
}
The thing is, it works, but when I populate the collection, it's not populated by App.Models.Booking, but with response's JSON.
Any idea?
Thanks a lot!
So, basically when you populate a collection, 3 flags are describing the behavior your method should have: add, remove, merge . We'll start by the default behavior of the set and add methods:
// Default options for `Collection#set`.
var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, merge: false, remove: false};
The add method in fact proxies the set method, as does the fetch method if you don't use the reset flag (which would cause to delete any model in your collection and create new ones each time you fetch them) which would call the reset method instead.
Now, how to use the flags. Well, it's the options specified in the doc. So basically, the default behavior for the add method is equivalent to this:
myCollection.add(myModels, {add: true, merge: false, remove: false});
Now, for the meaning of those flags:
- add: will add the news models (=the ones their id is not among the existing ones...) to the collection
- remove: will remove the old models (=the ones their id is not among the fetched models) of the collection
- merge: will update the attributes of the ones among the old and the fetched
What you should know about the merge flag: IT'S A REAL PAIN IN THE ASS. Really, I hate it. Why ? Because it uses an internal function that "prepares the models" :
if (!(model = this._prepareModel(models[i], options))) continue;
It means that it will create fake, volatile models. What's the big deal? Well, it means that it will execute the initialize function of those volatile models, possibly creating a chain reaction and unwanted behavior in your app.
So, what if you want this behavior but can't have volatile models created because it breaks your app? Well, you can set the merge flag to false and override the parse method to do it, something like:
parse: function(models) {
for(var i=0; i<models.length; i++) {
var model;
if(model = this.get(models[i].id)) {
model.set(models[i]);
}
}
return models;
}