I'd like to be able to search model attributes contained within a backbonejs collection. This is how I do it now...
wherePartial: function(attrs) {
// this method is really only tolerant of string values. you can't do partial
// matches on objects, but you can compare a list of strings. If you send it a list
// of values; attrs={keyA:[1,2,3],keyB:1}, etc the code will loop through the entire
// attrs obj and look for a match. strings are partially matched and if a list is found
// it's expected that it contains a list of string values. The string values should be considered
// to be like an OR operation in a query. Non-list items are like an AND.
if (_.isEmpty(attrs)) return [];
var matchFound = false;
return this.filter(function(model) {
// this is in the outer for loop so that a function isn't created on each iteration
function listComparator(value, index, list){
return model.get(key).toLowerCase().indexOf(value.toLowerCase()) >= 0;
}
for (var key in attrs) {
if (_.isArray(attrs[key])){
matchFound = _.any(attrs[key],listComparator);
if (matchFound !== true) return false;
} else {
matchFound = model.get(key).toLowerCase().indexOf(attrs[key].toLowerCase()) >= 0;
if (matchFound === false) return false;
}
}
return true;
});
}
Assume "C" is an instantiated collection, this is how I use it:
name:joe (nickname:joe the man nickname:joe cool nickname:joey)
is typed into a textbox and converted into this:
C.wherePartial({name:"joe",nicknames:["joe the man","joe cool","joey"]})
The above method returns all models that have the name joe and within that scope, any of the models that have the name joe and any of the nicknames. It works well for what I use it for. However, I'd really like to make a search that doesn't require the key:value pattern. I'd like to do this in a search box like when using a search engine on the web. I thought about just looking at every attribute on each model, but that takes awhile when you have a large collection (160k+ models).
Has anyone come across a need like this in the past? If so, how did you solve it? I'd like to keep the search contained on the client and not use any ajax calls to the backend. The reason for this is that the entire collection is already loaded on the client.
I thought of a way to do it. Serialize the attributes to a string during model instantiation. Listen for updates and update the serialization.
serializeAttr: function(){
this.serializedAttr = "";
var self = this;
_.each(this.toJSON(),function(value, key, list){
self.serializedAttr += value;
});
}
Then I can do simple searches on that cached value:
cc.serializedAttr.toLowerCase().indexOf("joe") >= 0
Related
I have an array of objects:
scope.values = [
{'key1':'valueA', 'key2': 'valueD'},
{'key1':'valueB'},
{'key1':'valueC'}
]
And I would like to filter a search input, which can contain multiple words separated by either comma or space:
<input ng-model="scope.search"></input>
We can list the array as follows:
<p ng-repeat="index, obj in scope.values | filter:scope.search"></p>
However, this only works for one input. What can I do when I have multiple inputs e.g. John Doe.
Note that I want it to be conditional. So not if John or Doe is found, but when John and Doe are found.
I don't think the built-in filter can do that. What you probably want is a custom filter, as described in the documentation here (about half way down the page) and in the official tutorial here.
For example, this custom filter should do what you want.
app.filter("multiSearch", [
function() {
//"data" is your searchable array, and "search" is the user's search string.
return function(data, search) {
//Get an array of search values by splitting the string on commas and spaces.
var searchArray = search.toLowerCase().split(/[, ]/);
//Use javascript's native Array.filter to decide which elements to cut and to keep.
return data.filter(function(item) {
//If the item contains ALL of the search words, keep it.
var foundCount = 0;
for (var searchElement of searchArray) {
for (var attribute in item) {
if (
String(item[attribute])
.toLowerCase()
.includes(searchElement)
) {
foundCount++;
break;
}
}
}
if (foundCount === searchArray.length) {
//Matched all search terms. Keep it.
return true;
}
else {
//Not a match. Cut it from the result.
return false;
}
});
};
}
]);
Then in your html you can call it like so:
<p ng-repeat="index, obj in scope.values | multiSearch:scope.search"></p>
Another thing you might consider, as suggested in the comments, is to forego using filters altogether and just run the logic inside your controller. You can use a lot of the logic provided in the example filter above -- you just have to implement your own system to run the filtering logic when the search query changes. There are benefits in avoiding filters in angularjs, but that is another topic.
When Indexing the items, it fails sometimes and it gives,
The remote server returned an error: (504) Gateway Timeout. [The remote server returned an error: (504) Gateway Timeout.]
The Indexing logic is here as below,
var client = EPiServer.Find.Framework.SearchClient.Instance;
List<ItemModel> items = getItems(); // Get more than 1000 items
List<ItemModel> tempItems = new List<ItemModel>();
//Index 50 items at a time
foreach(var item in items)
{
tempItems.Add(item);
if (tempItems.Count == 50)
{
client.Index(tempItems);
tempItems.Clear();
}
}
What causes this to happen ?
Note: The above mentioned ItemModel is a custom model which is not implemented interfaces (such as IContent). And the items is a list of ItemModel objects.
Additional info:
EPiServer.Find.Framework version 13.0.1
EPiServer.CMS.Core version 11.9.2
I always figured the SearchClient to be a bit sketchy when manipulating data in Find, as far as I figured (but I have to check this) the SearchClient obey under the request limitation of Episerver Find and when doing bigger operations in loops it tends to time out.
Instead, use the ContentIndexer, i.e.
// Use this or injected parameter
var loader = ServiceLocator.Current.GetInstance<IContentLoader>();
// Remove all children or not
var cascade = true;
ContentReference entryPoint = ...where you want to start
// Get all indexable languages from Find
Languages languages = SearchClient.Instance.Settings.Languages;
// Remove all current instances of all languages below the selected content node
//languages.ForEach(x => ContentIndexer.Instance.RemoveFromIndex(entryPoint, cascade.Checked, x.FieldSuffix));
foreach (var lang in languages)
{
if (cascade)
{
var descendents = loader.GetDescendents(entryPoint);
foreach (ContentReference descendent in descendents)
{
ContentIndexer.Instance.RemoveFromIndex(descendent, false, lang.FieldSuffix);
}
}
// Try delete the entrypoint
var entryTest = loader.Get<IContent>(entryPoint, new CultureInfo(lang.FieldSuffix));
if (entryTest != null)
{
var delRes = ContentIndexer.Instance.Delete(entryTest);
}
}
This is the most bulletproof way to delete stuff from the index as far as I figured.
So my question is: how do I scan the JSON in angular to find the first instance of isPrimary:true and then launch a function with the GUID that is in that item.
I have a webservice whos JSON defines available Accounts with a display name and a GUID this generates a dropdown select list that calls a function with the GUID included to return full data from a web service.
In the scenario where theres only 1 OPTION I dont show the SELECT and simply call the function with the single GUID to return the data from the service. If theres no options I dont show anything other than a message.
Code below shows what I currently have.
The Spec has now changed and the data they are sending me in the first service call which defines that select list is now including a property isPrimary:true on one of the JSON object along with its GUID as per the rest
I now need to change my interface to no longer use the SELECT list and instead fire the function call to the service for the item that contains the isPrimary:true property. However there may be multiple instances where isPrimary:true exists in the returning JSON so I just want to fire the function on the first found instance of isPrimary:true
Equally if that property isnt in any of the JSON items then just fire the function on the first item in the JSON.
My current Code is below - you can see the call to retrieve the full details is from function:
vm.retrieveAccount(GUID);
Where the GUID is supplied with each JSON object
Code is:
if (data.Accounts.length > 1) {
vm.hideAcc = false;
setBusyState(false);
//wait for the user to make a selection
} else if (data.Accounts.length == 1){
vm.hideAcc = true;
// Only 1 acc - no need for drop down get first item
vm.accSelected = data.Accounts[0].UniqueIdentifier;
vm.retrieveAccount(vm.accSelected);
} else {
// Theres no accounts
// Hide Drop down and show message
setBusyState(false);
vm.hideAcc = true;
setMessageState(false, true, "There are no Accounts")
}
Sample of new JSON structure
accName: "My Acc",
isPrimary: true,
GUID: "bg111010101"
Still think that's a weird spec, but simple enough to solve. Just step through the array and return the first isPrimary match. If none are found, return the first element of the array.
var findPrimary = function(data) {
if (!(Array.isArray(data)) || data.length == 0) {
return false; // not an array, or empty array
}
for (var i=0; i<data.length; i++) {
if (data[i].isPrimary) {
return data[i]; // first isPrimary match
}
}
// nothing had isPrimary, so return the first one:
return data[0];
}
I've been looking already around for the answer with no luck.
Basically I am reviewing again the Addy Osmani guide to Backbone, and it seems I can't get through this code here.
var people = new Backbone.Collection;
people.comparator = function(a, b) {
return a.get('name') < b.get('name') ? -1 : 1;
};
var tom = new Backbone.Model({name: 'Tom'});
var rob = new Backbone.Model({name: 'Rob'});
var tim = new Backbone.Model({name: 'Tim'});
people.add(tom);
people.add(rob);
people.add(tim);
console.log(people.indexOf(rob) === 0); // true
console.log(people.indexOf(tim) === 1); // true
console.log(people.indexOf(tom) === 2); // true
I don't see how people.comparator can reorder the collection even though is not called anywhere, plus how comes that returning 1 or -1 can reorder it.
Or is it implicitly called once the Collection is created or indexOf is called on the Collection itself?
From the backbone documentation:
By default there is no comparator for a collection. If you define a
comparator, it will be used to maintain the collection in sorted
order. This means that as models are added, they are inserted at the
correct index in collection.models.
So every time you call people.add(...) the collection uses the comparator that you have set with people.comparator = function(a, b) { ... } to insert the model in an ordered position.
I'm trying to filter a Collection, and then shuffle the filtered values.
I was thinking of using the where method Backbone provides. Something like:
myRandomModel = #.where({ someAttribute: true }).shuffle()[0]
However, where returns an array of all the models which match the attributes; and apparently shuffle needs a list to work with:
shuffle_ .shuffle(list)
Returns a shuffled copy of the list
http://documentcloud.github.com/underscore/#shuffle
Is there a way to turn my array of models into a 'list'? Or should I write some logic myself to get this done?
When the Underscore docs say list, they mean array. So you can use _.shuffle like this:
shuffled = _([1, 2, 3, 4]).shuffle()
Or in your case:
_(#where(someAttribute: true)).shuffle()
However, since you're just grabbing a single model, you could simply generate a random index instead of shuffling:
matches = #where(someAttribute: true)
a_model = matches[Math.floor(Math.random() * matches.length)]
The shuffle() and where() method are just a proxy in Backbone collections to the underscore method. Underscore methods still work on their own, with arrays as argument. Here is what I would do:
myRandomModel = _.shuffle(#.where({ someAttribute: true }))[0]
Reference: http://documentcloud.github.com/underscore/#shuffle
PS: #"mu is too short" is right however, to get a single model I would go the Math.random() way myself.
I put the following in my application.js file (using Rails 3):
Array.prototype.shuffleArray = function() {
var i = this.length, j, tempi, tempj;
if ( i === 0 ) return false;
while ( --i ) {
j = Math.floor( Math.random() * ( i + 1 ) );
tempi = this[i];
tempj = this[j];
this[i] = tempj;
this[j] = tempi;
}
return this;
};
and now I can call shuffleArray() on an array of arrays. Leaving this unanswered for now though, since I would like to know if there is a better way to do it with Underscore/Backbone.
First in your collection you should have a filtered function
Like
var MyCollection = Backbone.Collection.extend ({
filtered : function ( ) {
Normally you will use _.filter to get only the models you wanted but you may also use the suffle as a replacement use this.models to get the collection models
Here shuffle will mix the models
var results = _ .shuffle( this.models ) ;
Then use underscore map your results and transform it to JSON
like so
results = _.map( results, function( model ) { return model.toJSON() } );
finally returning a new backbone collection with only results you may return only the json if that's what you are looking for
return new Backbone.Collection( results ) ;
note if you don't want to keep all data in collection for later uses you may use the following and ignore the view below ;
this.reset( results ) ;
}
});