Underscore.js groupBy multiple values - arrays

Using Underscore.js, I'm trying to group a list of items multiple times, ie
Group by SIZE then for each SIZE, group by CATEGORY...
http://jsfiddle.net/rickysullivan/WTtXP/1/
Ideally, I'd like to have a function or extend _.groupBy() so that you can throw an array at it with the paramaters to group by.
var multiGroup = ['size', 'category'];
Probably could just make a mixin...
_.mixin({
groupByMulti: function(obj, val, arr) {
var result = {};
var iterator = typeof val == 'function' ? val : function(obj) {
return obj[val];
};
_.each(arr, function(arrvalue, arrIndex) {
_.each(obj, function(value, objIndex) {
var key = iterator(value, objIndex);
var arrresults = obj[objIndex][arrvalue];
if (_.has(value, arrvalue))
(result[arrIndex] || (result[arrIndex] = [])).push(value);
My head hurts, but I think some more pushing needs to go here...
});
})
return result;
}
});
properties = _.groupByMulti(properties, function(item) {
var testVal = item["size"];
if (parseFloat(testVal)) {
testVal = parseFloat(item["size"])
}
return testVal
}, multiGroup);

A simple recursive implementation:
_.mixin({
/*
* #mixin
*
* Splits a collection into sets, grouped by the result of running each value
* through iteratee. If iteratee is a string instead of a function, groups by
* the property named by iteratee on each of the values.
*
* #param {array|object} list - The collection to iterate over.
* #param {(string|function)[]} values - The iteratees to transform keys.
* #param {object=} context - The values are bound to the context object.
*
* #returns {Object} - Returns the composed aggregate object.
*/
groupByMulti: function(list, values, context) {
if (!values.length) {
return list;
}
var byFirst = _.groupBy(list, values[0], context),
rest = values.slice(1);
for (var prop in byFirst) {
byFirst[prop] = _.groupByMulti(byFirst[prop], rest, context);
}
return byFirst;
}
});
Demo in your jsfiddle

I think #Bergi's answer can be streamlined a bit by utilizing Lo-Dash's mapValues (for mapping functions over object values). It allows us to group the entries in an array by multiple keys in a nested fashion:
_ = require('lodash');
var _.nest = function (collection, keys) {
if (!keys.length) {
return collection;
}
else {
return _(collection).groupBy(keys[0]).mapValues(function(values) {
return nest(values, keys.slice(1));
}).value();
}
};
I renamed the method to nest because it ends up serving much the same role served by D3's nest operator. See this gist for details and this fiddle for demonstrated usage with your example.
lodash nest groupby

How about this rather simple hack?
console.log(_.groupBy(getProperties(), function(record){
return (record.size+record.category);
}));

Check out this underscore extension: Underscore.Nest, by Irene Ros.
This extension's output will be slightly different from what you specify, but the module is only about 100 lines of code, so you should be able to scan to get direction.

This is a great use case for the reduce phase of map-reduce. It's not going to be as visually elegant as the multi-group function (you can't just pass in an array of keys to group on), but overall this pattern gives you more flexibility to transform your data. EXAMPLE
var grouped = _.reduce(
properties,
function(buckets, property) {
// Find the correct bucket for the property
var bucket = _.findWhere(buckets, {size: property.size, category: property.category});
// Create a new bucket if needed.
if (!bucket) {
bucket = {
size: property.size,
category: property.category,
items: []
};
buckets.push(bucket);
}
// Add the property to the correct bucket
bucket.items.push(property);
return buckets;
},
[] // The starting buckets
);
console.log(grouped)
But if you just want it in an underscore mixin, here's my stab at it:
_.mixin({
'groupAndSort': function (items, sortList) {
var grouped = _.reduce(
items,
function (buckets, item) {
var searchCriteria = {};
_.each(sortList, function (searchProperty) { searchCriteria[searchProperty] = item[searchProperty]; });
var bucket = _.findWhere(buckets, searchCriteria);
if (!bucket) {
bucket = {};
_.each(sortList, function (property) { bucket[property] = item[property]; });
bucket._items = [];
buckets.push(bucket);
}
bucket._items.push(item);
return buckets;
},
[] // Initial buckets
);
grouped.sort(function (x, y) {
for (var i in sortList) {
var property = sortList[i];
if (x[property] != y[property])
return x[property] > y[property] ? 1 : -1;
}
return 0;
});
return _.map(grouped, function (group) {
var toReturn = { key: {}, value: group.__items };
_.each(sortList, function (searchProperty) { toReturn.key[searchProperty] = group[searchProperty]; });
return toReturn;
});
});

The improvements by joyrexus on bergi's method don't take advantage of the underscore/lodash mixin system. Here it is as a mixin:
_.mixin({
nest: function (collection, keys) {
if (!keys.length) {
return collection;
} else {
return _(collection).groupBy(keys[0]).mapValues(function(values) {
return _.nest(values, keys.slice(1));
}).value();
}
}
});

An example with lodash and mixin
_.mixin({
'groupByMulti': function (collection, keys) {
if (!keys.length) {
return collection;
} else {
return _.mapValues(_.groupBy(collection,_.first(keys)),function(values) {
return _.groupByMulti(values, _.rest(keys));
});
}
}
});

Here is an easy to understand function.
function mixin(list, properties){
function grouper(i, list){
if(i < properties.length){
var group = _.groupBy(list, function(item){
var value = item[properties[i]];
delete item[properties[i]];
return value;
});
_.keys(group).forEach(function(key){
group[key] = grouper(i+1, group[key]);
});
return group;
}else{
return list;
}
}
return grouper(0, list);
}

Grouping by a composite key tends to work better for me in most situations:
const groups = _.groupByComposite(myList, ['size', 'category']);
Demo using OP's fiddle
Mixin
_.mixin({
/*
* #groupByComposite
*
* Groups an array of objects by multiple properties. Uses _.groupBy under the covers,
* to group by a composite key, generated from the list of provided keys.
*
* #param {Object[]} collection - the array of objects.
* #param {string[]} keys - one or more property names to group by.
* #param {string} [delimiter=-] - a delimiter used in the creation of the composite key.
*
* #returns {Object} - the composed aggregate object.
*/
groupByComposite: (collection, keys, delimiter = '-') =>
_.groupBy(collection, (item) => {
const compositeKey = [];
_.each(keys, key => compositeKey.push(item[key]));
return compositeKey.join(delimiter);
}),
});

Related

Best way to get an element using its id from an array in Angular [duplicate]

I've got an array:
myArray = [{'id':'73','foo':'bar'},{'id':'45','foo':'bar'}, etc.]
I'm unable to change the structure of the array. I'm being passed an id of 45, and I want to get 'bar' for that object in the array.
How do I do this in JavaScript or using jQuery?
Use the find() method:
myArray.find(x => x.id === '45').foo;
From MDN:
The find() method returns the first value in the array, if an element in the array satisfies the provided testing function. Otherwise undefined is returned.
If you want to find its index instead, use findIndex():
myArray.findIndex(x => x.id === '45');
From MDN:
The findIndex() method returns the index of the first element in the array that satisfies the provided testing function. Otherwise -1 is returned.
If you want to get an array of matching elements, use the filter() method instead:
myArray.filter(x => x.id === '45');
This will return an array of objects. If you want to get an array of foo properties, you can do this with the map() method:
myArray.filter(x => x.id === '45').map(x => x.foo);
Side note: methods like find() or filter(), and arrow functions are not supported by older browsers (like IE), so if you want to support these browsers, you should transpile your code using Babel (with the polyfill).
As you are already using jQuery, you can use the grep function which is intended for searching an array:
var result = $.grep(myArray, function(e){ return e.id == id; });
The result is an array with the items found. If you know that the object is always there and that it only occurs once, you can just use result[0].foo to get the value. Otherwise you should check the length of the resulting array. Example:
if (result.length === 0) {
// no result found
} else if (result.length === 1) {
// property found, access the foo property using result[0].foo
} else {
// multiple items found
}
Another solution is to create a lookup object:
var lookup = {};
for (var i = 0, len = array.length; i < len; i++) {
lookup[array[i].id] = array[i];
}
... now you can use lookup[id]...
This is especially interesting if you need to do many lookups.
This won't need much more memory since the IDs and objects will be shared.
ECMAScript 2015 (JavaScript ES6) provides the find()
method on arrays:
var myArray = [
{id:1, name:"bob"},
{id:2, name:"dan"},
{id:3, name:"barb"},
]
// grab the Array item which matchs the id "2"
var item = myArray.find(item => item.id === 2);
// print
console.log(item.name);
It works without external libraries. But if you want older browser support you might want to include this polyfill.
Underscore.js has a nice method for that:
myArray = [{'id':'73','foo':'bar'},{'id':'45','foo':'bar'},etc.]
obj = _.find(myArray, function(obj) { return obj.id == '45' })
I think the easiest way would be the following, but it won't work on Internet Explorer 8 (or earlier):
var result = myArray.filter(function(v) {
return v.id === '45'; // Filter out the appropriate one
})[0].foo; // Get result and access the foo property
Try the following
function findById(source, id) {
for (var i = 0; i < source.length; i++) {
if (source[i].id === id) {
return source[i];
}
}
throw "Couldn't find object with id: " + id;
}
myArray.filter(function(a){ return a.id == some_id_you_want })[0]
A generic and more flexible version of the findById function above:
// array = [{key:value},{key:value}]
function objectFindByKey(array, key, value) {
for (var i = 0; i < array.length; i++) {
if (array[i][key] === value) {
return array[i];
}
}
return null;
}
var array = [{'id':'73','foo':'bar'},{'id':'45','foo':'bar'}];
var result_obj = objectFindByKey(array, 'id', '45');
Performance
Today 2020.06.20 I perform test on MacOs High Sierra on Chrome 81.0, Firefox 77.0 and Safari 13.1 for chosen solutions.
Conclusions for solutions which use precalculations
Solutions with precalculations (K,L) are (much much) faster than other solutions and will not be compared with them - probably they are use some special build-in browser optimisations
surprisingly on Chrome and Safari solution based on Map (K) are much faster than solution based on object {} (L)
surprisingly on Safari for small arrays solution based on object {} (L) is slower than traditional for (E)
surprisingly on Firefox for small arrays solution based on Map (K) is slower than traditional for (E)
Conclusions when searched objects ALWAYS exists
solution which use traditional for (E) is fastest for small arrays and fast for big arrays
solution using cache (J) is fastest for big arrays - surprisingly for small arrays is medium fast
solutions based on find (A) and findIndex (B) are fast for small arras and medium fast on big arrays
solution based on $.map (H) is slowest on small arrays
solution based on reduce (D) is slowest on big arrays
Conclusions when searched objects NEVER exists
solution based on traditional for (E) is fastest on small and big arrays (except Chrome-small arrays where it is second fast)
solution based on reduce (D) is slowest on big arrays
solution which use cache (J) is medium fast but can be speed up if we save in cache also keys which have null values (which was not done here because we want to avoid unlimited memory consumption in cache in case when many not existing keys will be searched)
Details
For solutions
without precalculations: A
B
C
D
E
F
G
H
I
J (the J solution use 'inner' cache and it speed depend on how often searched elements will repeat)
with precalculations
K
L
I perform four tests. In tests I want to find 5 objects in 10 loop iterations (the objects ID not change during iterations) - so I call tested method 50 times but only first 5 times have unique id values:
small array (10 elements) and searched object ALWAYS exists - you can perform it HERE
big array (10k elements) and searched object ALWAYS exist - you can perform it HERE
small array (10 elements) and searched object NEVER exists - you can perform it HERE
big array (10k elements) and searched object NEVER exists - you can perform it HERE
Tested codes are presented below
function A(arr, id) {
return arr.find(o=> o.id==id);
}
function B(arr, id) {
let idx= arr.findIndex(o=> o.id==id);
return arr[idx];
}
function C(arr, id) {
return arr.filter(o=> o.id==id)[0];
}
function D(arr, id) {
return arr.reduce((a, b) => (a.id==id && a) || (b.id == id && b));
}
function E(arr, id) {
for (var i = 0; i < arr.length; i++) if (arr[i].id==id) return arr[i];
return null;
}
function F(arr, id) {
var retObj ={};
$.each(arr, (index, obj) => {
if (obj.id == id) {
retObj = obj;
return false;
}
});
return retObj;
}
function G(arr, id) {
return $.grep(arr, e=> e.id == id )[0];
}
function H(arr, id) {
return $.map(myArray, function(val) {
return val.id == id ? val : null;
})[0];
}
function I(arr, id) {
return _.find(arr, o => o.id==id);
}
let J = (()=>{
let cache = new Map();
return function J(arr,id,el=null) {
return cache.get(id) || (el=arr.find(o=> o.id==id), cache.set(id,el), el);
}
})();
function K(arr, id) {
return mapK.get(id)
}
function L(arr, id) {
return mapL[id];
}
// -------------
// TEST
// -------------
console.log('Find id=5');
myArray = [...Array(10)].map((x,i)=> ({'id':`${i}`, 'foo':`bar_${i}`}));
const mapK = new Map( myArray.map(el => [el.id, el]) );
const mapL = {}; myArray.forEach(el => mapL[el.id]=el);
[A,B,C,D,E,F,G,H,I,J,K,L].forEach(f=> console.log(`${f.name}: ${JSON.stringify(f(myArray, '5'))}`));
console.log('Whole array',JSON.stringify(myArray));
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
This snippet only presents tested codes
Example tests results for Chrome for small array where searched objects always exists
As others have pointed out, .find() is the way to go when looking for one object within your array. However, if your object cannot be found using this method, your program will crash:
const myArray = [{'id':'73','foo':'bar'},{'id':'45','foo':'bar'}];
const res = myArray.find(x => x.id === '100').foo; // Uh oh!
/*
Error:
"Uncaught TypeError: Cannot read property 'foo' of undefined"
or in newer chrome versions:
Uncaught TypeError: Cannot read properties of undefined (reading 'foo')
*/
This can be fixed by checking whether the result of .find() is defined before using .foo on it. Modern JS allows us to do this easily with optional chaining, returning undefined if the object cannot be found, rather than crashing your code:
const myArray = [{'id':'73','foo':'bar'},{'id':'45','foo':'bar'}];
const res = myArray.find(x => x.id === '100')?.foo; // No error!
console.log(res); // undefined when the object cannot be found
If you do this multiple times, you may set up a Map (ES6):
const map = new Map( myArray.map(el => [el.id, el]) );
Then you can simply do a O(1) lookup:
map.get(27).foo
You can get this easily using the map() function:
myArray = [{'id':'73','foo':'bar'},{'id':'45','foo':'bar'}];
var found = $.map(myArray, function(val) {
return val.id == 45 ? val.foo : null;
});
//found[0] == "bar";
Working example: http://jsfiddle.net/hunter/Pxaua/
Using native Array.reduce
var array = [ {'id':'73' ,'foo':'bar'} , {'id':'45' ,'foo':'bar'} , ];
var id = 73;
var found = array.reduce(function(a, b){
return (a.id==id && a) || (b.id == id && b)
});
returns the object element if found, otherwise false
You can use filters,
function getById(id, myArray) {
return myArray.filter(function(obj) {
if(obj.id == id) {
return obj
}
})[0]
}
get_my_obj = getById(73, myArray);
While there are many correct answers here, many of them do not address the fact that this is an unnecessarily expensive operation if done more than once. In an extreme case this could be the cause of real performance problems.
In the real world, if you are processing a lot of items and performance is a concern it's much faster to initially build a lookup:
var items = [{'id':'73','foo':'bar'},{'id':'45','foo':'bar'}];
var lookup = items.reduce((o,i)=>o[i.id]=o,{});
you can then get at items in fixed time like this :
var bar = o[id];
You might also consider using a Map instead of an object as the lookup: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map
Recently, I have to face the same thing in which I need to search the string from a huge array.
After some search I found It'll be easy to handle with simple code:
Code:
var items = mydata.filter(function(item){
return item.word.toLowerCase().startsWith( 'gk );
})
See https://jsfiddle.net/maheshwaghmare/cfx3p40v/4/
Iterate over any item in the array. For every item you visit, check that item's id. If it's a match, return it.
If you just want teh codez:
function getId(array, id) {
for (var i = 0, len = array.length; i < len; i++) {
if (array[i].id === id) {
return array[i];
}
}
return null; // Nothing found
}
And the same thing using ECMAScript 5's Array methods:
function getId(array, id) {
var obj = array.filter(function (val) {
return val.id === id;
});
// Filter returns an array, and we just want the matching item.
return obj[0];
}
You may try out Sugarjs from http://sugarjs.com/.
It has a very sweet method on Arrays, .find. So you can find an element like this:
array.find( {id: 75} );
You may also pass an object with more properties to it to add another "where-clause".
Note that Sugarjs extends native objects, and some people consider this very evil...
As long as the browser supports ECMA-262, 5th edition (December 2009), this should work, almost one-liner:
var bFound = myArray.some(function (obj) {
return obj.id === 45;
});
Here's how I'd go about it in pure JavaScript, in the most minimal manner I can think of that works in ECMAScript 3 or later. It returns as soon as a match is found.
var getKeyValueById = function(array, key, id) {
var testArray = array.slice(), test;
while(test = testArray.pop()) {
if (test.id === id) {
return test[key];
}
}
// return undefined if no matching id is found in array
return;
}
var myArray = [{'id':'73', 'foo':'bar'}, {'id':'45', 'foo':'bar'}]
var result = getKeyValueById(myArray, 'foo', '45');
// result is 'bar', obtained from object with id of '45'
More generic and short
function findFromArray(array,key,value) {
return array.filter(function (element) {
return element[key] == value;
}).shift();
}
in your case Ex. var element = findFromArray(myArray,'id',45) that will give you the whole element.
We can use Jquery methods $.each()/$.grep()
var data= [];
$.each(array,function(i){if(n !== 5 && i > 4){data.push(item)}}
or
var data = $.grep(array, function( n, i ) {
return ( n !== 5 && i > 4 );
});
use ES6 syntax:
Array.find, Array.filter, Array.forEach, Array.map
Or use Lodash https://lodash.com/docs/4.17.10#filter, Underscore https://underscorejs.org/#filter
Building on the accepted answer:
jQuery:
var foo = $.grep(myArray, function(e){ return e.id === foo_id})
myArray.pop(foo)
Or CoffeeScript:
foo = $.grep myArray, (e) -> e.id == foo_id
myArray.pop foo
Use Array.prototype.filter() function.
DEMO: https://jsfiddle.net/sumitridhal/r0cz0w5o/4/
JSON
var jsonObj =[
{
"name": "Me",
"info": {
"age": "15",
"favColor": "Green",
"pets": true
}
},
{
"name": "Alex",
"info": {
"age": "16",
"favColor": "orange",
"pets": false
}
},
{
"name": "Kyle",
"info": {
"age": "15",
"favColor": "Blue",
"pets": false
}
}
];
FILTER
var getPerson = function(name){
return jsonObj.filter(function(obj) {
return obj.name === name;
});
}
You can do this even in pure JavaScript by using the in built "filter" function for arrays:
Array.prototype.filterObjects = function(key, value) {
return this.filter(function(x) { return x[key] === value; })
}
So now simply pass "id" in place of key and "45" in place of value, and you will get the full object matching an id of 45. So that would be,
myArr.filterObjects("id", "45");
I really liked the answer provided by Aaron Digulla but needed to keep my array of objects so I could iterate through it later. So I modified it to
var indexer = {};
for (var i = 0; i < array.length; i++) {
indexer[array[i].id] = parseInt(i);
}
//Then you can access object properties in your array using
array[indexer[id]].property
Use:
var retObj ={};
$.each(ArrayOfObjects, function (index, obj) {
if (obj.id === '5') { // id.toString() if it is int
retObj = obj;
return false;
}
});
return retObj;
It should return an object by id.
This solution may helpful as well:
Array.prototype.grep = function (key, value) {
var that = this, ret = [];
this.forEach(function (elem, index) {
if (elem[key] === value) {
ret.push(that[index]);
}
});
return ret.length < 2 ? ret[0] : ret;
};
var bar = myArray.grep("id","45");
I made it just like $.grep and if one object is find out, function will return the object, rather than an array.
Dynamic cached find
In this solution, when we search for some object, we save it in cache. This is middle point between "always search solutions" and "create hash-map for each object in precalculations".
let cachedFind = (()=>{
let cache = new Map();
return (arr,id,el=null) =>
cache.get(id) || (el=arr.find(o=> o.id==id), cache.set(id,el), el);
})();
// ---------
// TEST
// ---------
let myArray = [...Array(100000)].map((x,i)=> ({'id':`${i}`, 'foo':`bar_${i}`}));
// example usage
console.log( cachedFind(myArray,'1234').foo );
// Benchmark
let bench = (id) => {
console.time ('time for '+id );
console.log ( cachedFind(myArray,id).foo ); // FIND
console.timeEnd('time for '+id );
}
console.log('----- no cached -----');
bench(50000);
bench(79980);
bench(99990);
console.log('----- cached -----');
bench(79980); // cached
bench(99990); // cached

AngularJS server-side multi-column search

I am building an application in NodeJS and AngularJS.
I am building a multi-column search functionality where the user can type in search keywords into separate searchboxes (at the top of each column) and retrieve the results based on the column.
So far I have a single searchbox that searches all attributes at the same time.
How can I implement multiple individual searchboxes that will return results based on multiple attributes?
Note: I want to implement this on the server-side for performance reasons. (I know that I can simply use HTML attributes | filter:column1 | filter:column2 but want to avoid this technique if possible).
Here is the code I have so far. I am thinking that I need to pass in some sort of "searchBy" variable that is set on the view and then update the search method to search by multiple query/attribute pairs.
//Search service factory
//Initialize filtered items and get search results
function search(items, query) {
this.filteredItems = $filter('filter')(items, function (item) {
for(var attr in item) {
if (searchMatch(item[attr], query))
return true;
}
return false;
});
return this.filteredItems;
}
function searchMatch(haystack, needle) {
if (!needle) {
return true;
}
return haystack.toString().toLowerCase().indexOf(needle.toLowerCase()) !== -1;
};
//Controller
vm.filteredItems = vm.search(vm.unfilteredItems, vm.query);
//View
input(type='text', ng-model='vm.query', ng-change='vm.search(vm.unfilteredItems, vm.query)', placeholder='Search')
I was able to solve this by first creating an array of objects for each search box then repeating those boxes in the view with the ng-repeat attribute.
//Controller
var vm = this;
var vm.unfilteredItems; //data source query removed for brevity
//Initialize search inputs
vm.search_by_inputs = [
{search_column: 'id', search_query: ''},
{search_column: 'requester', search_query: ''},
{search_column: 'dataowner', search_query: ''}
];
function initSearch() {
vm.filtered_items = vm.search(vm.unfiltered_items, vm.search_by_inputs);
}
//View
input.input-large.search-query(type='text', value='{{search_by.search_query}}', ng-model='search_by.search_query' ng-change='vm.initSearch()', placeholder='Search')
The next step is to loop over the search_by_inputs object in the controller and create a new object with only the inputs that have search values entered into the searchboxes in the view. Then in the search method the built-in "filter" component iterates each item, and inside that loop each of the search terms is checked against that value with the column name that matches the property.
/*
* Create new array of objects with only elements that have search values to optimize loop inside filter
* #search_by_inputs array of objects each has a key search_column and a value search_query
*/
function optimizeSearchProperties(search_by_inputs) {
search_by_properties = [];
for (var i = 0, len = search_by_inputs.length; i < len; i++) {
//If this column input box has query text
if (search_by_inputs[i].search_query) {
search_by_properties.push(search_by_inputs[i]);
}
}
return search_by_properties;
}
/*
* #haystack search item
* #needle search term
*/
function searchMatch(haystack, needle) {
if (!needle) {
return true;
}
return haystack.toString().toLowerCase().indexOf(needle.toLowerCase()) !== -1;
}
/*
* Create filtered items object by filtering search results
* #items original array of objects returned by database query result
* #search_by_inputs array of objects each has a key search_column and a value search_query
*/
function search(items, search_by_inputs) {
var search_by_properties = optimizeSearchProperties(search_by_inputs);
//If there are no search properties input by requester then return all items
if (search_by_properties.length === 0) {
this.filtered_items = items;
return this.filtered_items;
}
this.filtered_items = $filter('filter')(items, function (item) {
var search_result = true;
//Loop over all search by input textboxes
for (var n = 0, len = search_by_properties.length; n < len; n++) {
//If there is no query text
if (!search_by_properties[n].search_query) {
//Continue to next element in array
continue;
//Else if element has a property that matches search input column name
} else if (item[search_by_properties[n].search_column]) {
if (!searchMatch(item[search_by_properties[n].search_column], search_by_properties[n].search_query)) {
search_result = false;
break;
}
}
}
return search_result;
});
return this.filtered_items;
}
I would be glad to have some feedback on this solution in terms of optimization, performance, technique, etc. Thanks!

Is [] parameter in the AngularJS module definition compulsory?

In definition of AngularJS module, [] is a parameter for other depended module on this module.
<script>
var app = angular.module('myApp',[]);
app.controller("myCtrl", function($scope) {
$scope.firstName = "John";
$scope.lastName = "Doe";
});
</script>
My Question is,
Is this parameter [] necessary, because the the following link or example they didn't mentioned [] parameter, but in above example(w3schooles), if we remove '[]' parameter then code will not give correct output see it?
Please see the this link openstack, they are not using [] parameter
var module = angular.module('hz.dashboard.launch-instance');
/**
* #ngdoc service
* #name launchInstanceModel
*
* #description
* This is the M part in MVC design pattern for launch instance
* wizard workflow. It is responsible for providing data to the
* view of each step in launch instance workflow and collecting
* user's input from view for creation of new instance. It is
* also the center point of communication between launch instance
* UI and services API.
*/
module.factory('launchInstanceModel', ['$q',
'cinderAPI',
'glanceAPI',
'keystoneAPI',
'neutronAPI',
'novaAPI',
'novaExtensions',
'securityGroup',
'serviceCatalog',
function ($q,
cinderAPI,
glanceAPI,
keystoneAPI,
neutronAPI,
novaAPI,
novaExtensions,
securityGroup,
serviceCatalog) {
var initPromise,
allNamespacesPromise;
// Constants (const in ES6)
var NON_BOOTABLE_IMAGE_TYPES = ['aki', 'ari'],
SOURCE_TYPE_IMAGE = 'image',
SOURCE_TYPE_SNAPSHOT = 'snapshot',
SOURCE_TYPE_VOLUME = 'volume',
SOURCE_TYPE_VOLUME_SNAPSHOT = 'volume_snapshot';
/**
* #ngdoc model api object
*/
var model = {
initializing: false,
initialized: false,
/**
* #name newInstanceSpec
*
* #description
* A dictionary like object containing specification collected from user's
* input. Its required properties include:
*
* #property {String} name: The new server name.
* #property {String} source_type: The type of source
* Valid options: (image | snapshot | volume | volume_snapshot)
* #property {String} source_id: The ID of the image / volume to use.
* #property {String} flavor_id: The ID of the flavor to use.
*
* Other parameters are accepted as per the underlying novaclient:
* - https://github.com/openstack/python-novaclient/blob/master/novaclient/v2/servers.py#L417
* But may be required additional values as per nova:
* - https://github.com/openstack/horizon/blob/master/openstack_dashboard/api/rest/nova.py#L127
*
* The JS code only needs to set the values below as they are made.
* The createInstance function will map them appropriately.
*/
// see initializeNewInstanceSpec
newInstanceSpec: {},
/**
* cloud service properties, they should be READ-ONLY to all UI controllers
*/
availabilityZones: [],
flavors: [],
allowedBootSources: [],
images: [],
allowCreateVolumeFromImage: false,
arePortProfilesSupported: false,
imageSnapshots: [],
keypairs: [],
metadataDefs: {
flavor: null,
image: null,
volume: null
},
networks: [],
neutronEnabled: false,
novaLimits: {},
profiles: [],
securityGroups: [],
volumeBootable: false,
volumes: [],
volumeSnapshots: [],
/**
* api methods for UI controllers
*/
initialize: initialize,
createInstance: createInstance
};
// Local function.
function initializeNewInstanceSpec(){
model.newInstanceSpec = {
availability_zone: null,
admin_pass: null,
config_drive: false,
user_data: '', // REQUIRED Server Key. Null allowed.
disk_config: 'AUTO',
flavor: null, // REQUIRED
instance_count: 1,
key_pair: [], // REQUIRED Server Key
name: null, // REQUIRED
networks: [],
profile: {},
security_groups: [], // REQUIRED Server Key. May be empty.
source_type: null, // REQUIRED for JS logic (image | snapshot | volume | volume_snapshot)
source: [],
vol_create: false, // REQUIRED for JS logic
vol_device_name: 'vda', // May be null
vol_delete_on_terminate: false,
vol_size: 1
};
}
/**
* #ngdoc method
* #name launchInstanceModel.initialize
* #returns {promise}
*
* #description
* Send request to get all data to initialize the model.
*/
function initialize(deep) {
var deferred, promise;
// Each time opening launch instance wizard, we need to do this, or
// we can call the whole methods `reset` instead of `initialize`.
initializeNewInstanceSpec();
if (model.initializing) {
promise = initPromise;
} else if (model.initialized && !deep) {
deferred = $q.defer();
promise = deferred.promise;
deferred.resolve();
} else {
model.initializing = true;
model.allowedBootSources.length = 0;
promise = $q.all([
getImages(),
novaAPI.getAvailabilityZones().then(onGetAvailabilityZones, noop),
novaAPI.getFlavors(true, true).then(onGetFlavors, noop),
novaAPI.getKeypairs().then(onGetKeypairs, noop),
novaAPI.getLimits().then(onGetNovaLimits, noop),
securityGroup.query().then(onGetSecurityGroups, noop),
serviceCatalog.ifTypeEnabled('network').then(getNetworks, noop),
serviceCatalog.ifTypeEnabled('volume').then(getVolumes, noop)
]);
promise.then(
function() {
model.initializing = false;
model.initialized = true;
// This provides supplemental data non-critical to launching
// an instance. Therefore we load it only if the critical data
// all loads successfully.
getMetadataDefinitions();
},
function () {
model.initializing = false;
model.initialized = false;
}
);
}
return promise;
}
/**
* #ngdoc method
* #name launchInstanceModel.createInstance
* #returns {promise}
*
* #description
* Send request for creating server.
*/
function createInstance() {
var finalSpec = angular.copy(model.newInstanceSpec);
cleanNullProperties();
setFinalSpecBootsource(finalSpec);
setFinalSpecFlavor(finalSpec);
setFinalSpecNetworks(finalSpec);
setFinalSpecKeyPairs(finalSpec);
setFinalSpecSecurityGroups(finalSpec);
return novaAPI.createServer(finalSpec);
}
function cleanNullProperties(finalSpec){
// Initially clean fields that don't have any value.
for (var key in finalSpec) {
if (finalSpec.hasOwnProperty(key) && finalSpec[key] === null) {
delete finalSpec[key];
}
}
}
//
// Local
//
function onGetAvailabilityZones(data) {
model.availabilityZones.length = 0;
push.apply(model.availabilityZones, data.data.items
.filter(function (zone) {
return zone.zoneState && zone.zoneState.available;
})
.map(function (zone) {
return zone.zoneName;
})
);
if(model.availabilityZones.length > 0) {
model.newInstanceSpec.availability_zone = model.availabilityZones[0];
}
}
// Flavors
function onGetFlavors(data) {
model.flavors.length = 0;
push.apply(model.flavors, data.data.items);
}
function setFinalSpecFlavor(finalSpec) {
if ( finalSpec.flavor ) {
finalSpec.flavor_id = finalSpec.flavor.id;
} else {
delete finalSpec.flavor_id;
}
delete finalSpec.flavor;
}
// Keypairs
function onGetKeypairs(data) {
angular.extend(
model.keypairs,
data.data.items.map(function (e) {
e.keypair.id = e.keypair.name;
return e.keypair;
}));
}
function setFinalSpecKeyPairs(finalSpec) {
// Nova only wants the key name. It is a required field, even if None.
if(!finalSpec.key_name && finalSpec.key_pair.length === 1){
finalSpec.key_name = finalSpec.key_pair[0].name;
} else if (!finalSpec.key_name) {
finalSpec.key_name = null;
}
delete finalSpec.key_pair;
}
// Security Groups
function onGetSecurityGroups(data) {
model.securityGroups.length = 0;
push.apply(model.securityGroups, data.data.items);
// set initial default
if (model.newInstanceSpec.security_groups.length === 0 &&
model.securityGroups.length > 0) {
model.securityGroups.forEach(function (securityGroup) {
if (securityGroup.name === 'default') {
model.newInstanceSpec.security_groups.push(securityGroup);
}
});
}
}
function setFinalSpecSecurityGroups(finalSpec) {
// pull out the ids from the security groups objects
var security_group_ids = [];
finalSpec.security_groups.forEach(function(securityGroup){
if(model.neutronEnabled) {
security_group_ids.push(securityGroup.id);
} else {
security_group_ids.push(securityGroup.name);
}
});
finalSpec.security_groups = security_group_ids;
}
// Networks
function getNetworks() {
return neutronAPI.getNetworks().then(onGetNetworks, noop);
}
function onGetNetworks(data) {
model.neutronEnabled = true;
model.networks.length = 0;
push.apply(model.networks, data.data.items);
}
function setFinalSpecNetworks(finalSpec) {
finalSpec.nics = [];
finalSpec.networks.forEach(function (network) {
finalSpec.nics.push(
{
"net-id": network.id,
"v4-fixed-ip": ""
});
});
delete finalSpec.networks;
}
// Boot Source
function getImages(){
return glanceAPI.getImages({status:'active'}).then(onGetImages);
}
function isBootableImageType(image){
// This is a blacklist of images that can not be booted.
// If the image container type is in the blacklist
// The evaluation will result in a 0 or greater index.
return NON_BOOTABLE_IMAGE_TYPES.indexOf(image.container_format) < 0;
}
function onGetImages(data) {
model.images.length = 0;
push.apply(model.images, data.data.items.filter(function (image) {
return isBootableImageType(image) &&
(!image.properties || image.properties.image_type !== 'snapshot');
}));
addAllowedBootSource(model.images, SOURCE_TYPE_IMAGE, gettext('Image'));
model.imageSnapshots.length = 0;
push.apply(model.imageSnapshots,data.data.items.filter(function (image) {
return isBootableImageType(image) &&
(image.properties && image.properties.image_type === 'snapshot');
}));
addAllowedBootSource(model.imageSnapshots, SOURCE_TYPE_SNAPSHOT, gettext('Instance Snapshot'));
}
function getVolumes(){
var volumePromises = [];
// Need to check if Volume service is enabled before getting volumes
model.volumeBootable = true;
addAllowedBootSource(model.volumes, SOURCE_TYPE_VOLUME, gettext('Volume'));
addAllowedBootSource(model.volumeSnapshots, SOURCE_TYPE_VOLUME_SNAPSHOT, gettext('Volume Snapshot'));
volumePromises.push(cinderAPI.getVolumes({ status: 'available', bootable: 1 }).then(onGetVolumes));
volumePromises.push(cinderAPI.getVolumeSnapshots({ status: 'available' }).then(onGetVolumeSnapshots));
// Can only boot image to volume if the Nova extension is enabled.
novaExtensions.ifNameEnabled('BlockDeviceMappingV2Boot')
.then(function(){ model.allowCreateVolumeFromImage = true; });
return $q.all(volumePromises);
}
function onGetVolumes(data) {
model.volumes.length = 0;
push.apply(model.volumes, data.data.items);
}
function onGetVolumeSnapshots(data) {
model.volumeSnapshots.length = 0;
push.apply(model.volumeSnapshots, data.data.items);
}
function addAllowedBootSource(rawTypes, type, label) {
if (rawTypes && rawTypes.length > 0) {
model.allowedBootSources.push({
type: type,
label: label
});
}
}
function setFinalSpecBootsource(finalSpec) {
finalSpec.source_id = finalSpec.source && finalSpec.source[0] && finalSpec.source[0].id;
delete finalSpec.source;
switch (finalSpec.source_type.type) {
case SOURCE_TYPE_IMAGE:
setFinalSpecBootImageToVolume(finalSpec);
break;
case SOURCE_TYPE_SNAPSHOT:
break;
case SOURCE_TYPE_VOLUME:
setFinalSpecBootFromVolumeDevice(finalSpec, 'vol');
break;
case SOURCE_TYPE_VOLUME_SNAPSHOT:
setFinalSpecBootFromVolumeDevice(finalSpec, 'snap');
break;
default:
// error condition
console.log("Unknown source type: " + finalSpec.source_type);
}
// The following are all fields gathered into simple fields by
// steps so that the view can simply bind to simple model attributes
// that are then transformed a single time to Nova's expectation
// at launch time.
delete finalSpec.source_type;
delete finalSpec.vol_create;
delete finalSpec.vol_device_name;
delete finalSpec.vol_delete_on_terminate;
delete finalSpec.vol_size;
}
function setFinalSpecBootImageToVolume(finalSpec){
if(finalSpec.vol_create) {
// Specify null to get Autoselection (not empty string)
var device_name = finalSpec.vol_device_name ? finalSpec.vol_device_name : null;
finalSpec.block_device_mapping_v2 = [];
finalSpec.block_device_mapping_v2.push(
{
'device_name': device_name,
'source_type': SOURCE_TYPE_IMAGE,
'destination_type': SOURCE_TYPE_VOLUME,
'delete_on_termination': finalSpec.vol_delete_on_terminate ? 1 : 0,
'uuid': finalSpec.source_id,
'boot_index': '0',
'volume_size': finalSpec.vol_size
}
);
}
}
function setFinalSpecBootFromVolumeDevice(finalSpec, sourceType) {
finalSpec.block_device_mapping = {};
finalSpec.block_device_mapping[finalSpec.vol_device_name] = [
finalSpec.source_id,
':',
sourceType,
'::',
(finalSpec.vol_delete_on_terminate ? 1 : 0)
].join('');
// Source ID must be empty for API
finalSpec.source_id = '';
}
// Nova Limits
function onGetNovaLimits(data) {
angular.extend(model.novaLimits, data.data);
}
// Metadata Definitions
/**
* Metadata definitions provide supplemental information in detail
* rows and should not slow down any of the other load processes.
* All code should be written to treat metadata definitions as
* optional, because they are never guaranteed to exist.
*/
function getMetadataDefinitions() {
// Metadata definitions often apply to multiple
// resource types. It is optimal to make a single
// request for all desired resource types.
var resourceTypes = {
flavor: 'OS::Nova::Flavor',
image: 'OS::Glance::Image',
volume: 'OS::Cinder::Volumes'
};
angular.forEach(resourceTypes, function (resourceType, key) {
glanceAPI.getNamespaces({
'resource_type': resourceType
}, true)
.then(function (data) {
var namespaces = data.data.items;
// This will ensure that the metaDefs model object remains
// unchanged until metadefs are fully loaded. Otherwise,
// partial results are loaded and can result in some odd
// display behavior.
if(namespaces.length) {
model.metadataDefs[key] = namespaces;
}
});
});
}
return model;
}
]);
})();
Status
The array or [] parameter is needed to specify dependent modules when you declare your own module, so this should only happen once per module.
The second notation, without the parameter is just retrieving the module so you can attach controllers/services/filters/... to it.
Use the array notation for the declaration of your module, use the single parameter notation if you want to add something to it.
For example:
in app.module.js
//You want to make use of the ngRoute module,
//so you have to specify a dependency on it
angular.module('app', ['ngRoute']);
You will only specify the dependencies on your module once, when you declare it.
in main.controller.js
//You want to add a controller to your module, so you want to retrieve your module
angular.module('app').controller('mainCtrl', mainCtrl);
function mainCtrl() { };
Now angular will try to find a module by that name instead of creating one, when it doesn't find one, you'll get some errors, which explains your original question.
You will typically do this every time you want to add something to your module.
Note that you could also achieve this by storing your module in a global variable when you create it and then access the module by that variable when you want to add things to it, however as you probably know, creating global variables is a bad practice.
Facing an error with angular is a bliss because it provides the link to description of the error in the console.
From an example page like that...
When defining a module with no module dependencies, the array of dependencies should be defined and empty.
var myApp = angular.module('myApp', []);
To retrieve a reference to the same module for further configuration, call angular.module without the array argument.
var myApp = angular.module('myApp');

Create an AngularJS factory / object to hold an array

How can I update this code to hold an array of values? I want to hold FIELDNAME and the VALUE.
I want to set / add to the list by doing the following - add a value to the array/list.
userFilters.setData(' lastname', 'smith');
userFilters.setData(' firstname', 'bob');
userFilters.setData(' Mi', 'D');
And have the object hold an Array of
'lastname','smith'
'firstname','bob'
'mi','D'
App.factory('userFilters', [function () {
var data = {};
var getData = function (field) {
return data[field];
};
var setData = function (field, value) {
data[field] = value;
};
return {
getData: getData,
setData: setData
}
}]);
You could recreate this fairly simply without any need for the methods you are creating.
Javascript Objects are designed to do exactly what you are looking for here.
App.factory('userFilters', function() {
return {};
});
Rather than using a getter and setter, you could instead get and set values with square bracket accessors.
// setting properties
userFilters['lastname'] = 'smith';
userFilters['firstname'] = 'bob';
userFilters['Mi'] = 'D';
// getting properties
userFilters['lastname']; // 'smith'
userFilters['firstname']; // 'smith'
If you want to be able to have full control of what happens at get/set time, you could look at intercepting these calls with internal getters and setters, providing you know the property names before hand.
Finally, you could also wrap your own get and set functions around the object in order to hide it. However, this would make more sense as a Service.
App.service('userFilters', function() {
var store = {};
this.get = function(key) {
return store[key];
};
this.set = function(key, value) {
store[key] = value;
};
});
If it's important that your factory/service exposes an array then I would recommend sticking to using an object to store keys and values, but adding an array export method.
App.service('userFilters', function() {
var store = {};
this.toArray = function() {
var records = [];
return Object.keys(store).map(function(key) {
records.push([key, store[key]]);
});
};
this.get = function(key) {
return store[key];
};
this.set = function(key, value) {
store[key] = value;
};
});
If you want to loop through the properties, you can use a for-in loop.
for(var key in userFilters) {
var value = userFilters[key];
console.log(key, value);
}
You can also check whether there any keys at all, using the Object.keys method.
Object.keys(userFilters); // ['lastname', 'firstname', 'Mi']
This will return an array of all the keys in the object. If it has length 0, then you know it's empty.
In order to use an array for storage you will need to update both getData and setData methods and define data as an array []:
App.factory('userFilters', [function () {
var data = [];
var getData = function (field) {
for(var i=0; i<data.length; i+=2) {
if(data[i] == field) {
return data[i+1];
}
}
return null;
};
var setData = function (field, value) {
data = data.concat([field, value]);
};
return {
getData: getData,
setData: setData
}
}]);

How to make a function that returns an array with dynamic key names for NodeJS?

I'm working on a RESTful api built with NodeJS, express, express-resource, and sequelize for MySQL ORM. I want to be able to retrieve a set of records and respond with res.json(records). However, I am unable to directly use the collection returned by sequelize. When I try, I get the following error:
TypeError: Converting circular structure to JSON
As a work around, I created a function that takes the records input and returns an array:
/**
* takes an Array of records and
* returns a collection
*
* #param {Array} recs
* #return {Array}
* #api public
*/
function recs2Array(recs){
for(var c = 0; c < recs.length; c++){
var collection = [];
(function(c){
// this is crap and must be done for every query
var data = {
id: recs[c]['id'],
gender: recs[c]['gender']
};
collection.push(data);
// debugger;
})(c);
};
return collection;
};
Now, this is terribly inefficient because I have to manually define all of the array keys that are to be returned. This means I have to use this pattern everywhere I want to res.json() for each model.find() of model.findAll(). In one of my resource controllers I have to do this for multiple collections resulting lots of extra code.
To make this better, I'm trying to create a function that will generate the array keys dynamically:
/**
* takes an Array of records
* and and Array of fields and returns
* a collection.
*
* #param {Array} recs
* #param {Array} fields
* #return {Array}
* #api public
*/
function recs2ray(recs, fields){
for(var c = 0; c < recs.length; c++){
fields = fields || null;
var collection = [];
(function(c){
for(var i = 0; i < fields.length; i++){
(function(i){
// how do I create dynamic,
// variable key names?
})(i);
}
debugger;
}
};
};
Unfortunately, I'm not really sure how to make dynamic array keys. I was thinking that I would pass in a predefined array that contains fields that will be mapped in the function call:
var recs = recs2Array(genders, {['id', 'gender']});
When I do this I get the feeling am undermining sequelize ORM in some sense. So, My question is, "How can I dynamically map fields from the array that is returned by sequelize's model.findAll() to an array that is pushed to a collection that is returned to the caller?"
var input = ...;
var result = input.map(function(row) {
var result = {};
['id', 'gender'].forEach(function(key) {
result[key] = row[key];
});
return result;
});
Or if you want to do it with a helper method:
function purify(obj, keys) {
return obj.map(function(row) {
var result = {};
keys.forEach(function(key) {
result[key] = row[key];
});
return result;
});
}
var result = purify(..., ['id', 'gender']);

Resources