Show an aggregated list in angularjs - javascript

In my model I have data similar to:
$scope.list = [{id:0,tags:['tag1','tag2']},{id:2,tags:['tag2']}};
I want to show a list of tags (contains unique values of 'tag1' and 'tag2') with checkboxes. Hopefully something like:
<div ng-repeat="tag in list.tags">
<label class="checkbox">
<input type="checkbox" ng-model="filter.tag" />
{{tag}}
</label>
</div>
I know how to filter the main list based on whats checked if I hard code the list, but not how to generate the list of unique tags automatically.

You are looking to perform three operations:
Get the array of tags from each item in $scope.list
Flatten these into a single array
Get the unique values from this array
You can do this with pure JavaScript, but to make things easier, I would recommend using Underscore, a library that gives you access to many functions for manipulating and inspecting arrays, objects, and so forth.
Let's start with this code:
$scope.list = [
{id: 0, tags: ['tag1', 'tag2']},
{id: 1, tags: ['tag2']},
{id: 2, tags: ['tag1', 'tag3', 'tag4']},
{id: 3, tags: ['tag3', 'tag4']}
];
Now, let's perform the first operation: get the array from the tags property for each object in $scope.list. Underscore provides the pluck method, which is just what we need.
pluck _.pluck(list, propertyName)
A convenient version of what is perhaps the most common use-case for map: extracting a list of property values.
Using pluck, we can get the following:
var tags = _.pluck($scope.list, 'tags');
// gives us [['tag1', 'tag2'], ['tag2'], ['tag1', 'tag3', 'tag4'], ['tag3', 'tag4']]
Now, we want to flatten that array.
flatten _.flatten(array, [shallow])
Flattens a nested array (the nesting can be to any depth). If you pass shallow, the array will only be flattened a single level.
tags = _.flatten(tags);
// gives us ['tag1', 'tag2', 'tag2', 'tag1', 'tag3', 'tag4', 'tag3', 'tag4']
Finally, you only want one instance of each tag.
uniq _.uniq(array, [isSorted], [iterator]) Alias: unique
Produces a duplicate-free version of the array, using === to test object equality. If you know in advance that the array is sorted, passing true for isSorted will run a much faster algorithm. If you want to compute unique items based on a transformation, pass an iterator function.
tags = _.unique(tags)
// gives us ['tag1', 'tag2', 'tag3', 'tag4']
We can combine these together with Underscore's useful chain method to chain these together. Let's create a function on the scope that returns the unique tags:
$scope.uniqueTags = function() {
return _.chain($scope.list)
.pluck('tags')
.flatten()
.unique()
.value();
};
Since this is a function, it will always return the unique tags, no matter if we add or remove items in $scope.list after the fact.
Now you can use ng-repeat on uniqueTags to show each tag:
<div ng-repeat="tag in uniqueTags()">
<label class="checkbox">
<input type="checkbox" ng-model="filter.tag" />
{{tag}}
</label>
</div>
Here is a working jsFiddle that demonstrates this technique: http://jsfiddle.net/BinaryMuse/cqTKG/

Use a custom filter to get a unique set/array of tags, suitable for use with ng-repeat:
.filter('uniqueTags', function() {
return function(list) {
var tags = {};
angular.forEach(list, function(obj, key) {
angular.forEach(obj.tags, function(value) {
tags[value] = 1;
})
});
var uniqueTags = []
for (var key in tags) {
uniqueTags.push(key);
}
return uniqueTags;
}
});
I first put the tags into an object, which automatically gives us uniqueness. Then I convert it to an array.
Use as follows:
<div ng-repeat="tag in list | uniqueTags">
Fiddle.
The following may not do what I think you probably want/expect it to do:
<input type="checkbox" ng-model="filter.tag">
This does not create $scope properties filter.tag1 and filter.tag2 on the controller scope (i.e., the scope where ng-repeat is used). Each iteration of ng-repeat creates its own child scope, so the ng-model above will create scope property filter.tag on each ng-repeat child scope, as shown in my fiddle.

Related

Find if already exists, JS and Lodash

So I have an interesting issue I am not sure how to follow, I need to use lodash to search two arrays in an object, looking to see if x already exists, lets look at a console out put:
There are two keys I am interested in: questChains and singleQuests, I want to write two seperate functions using lodash to say: find me id x in the array of objects where questChains questChainID is equal to x.
The second function would say: Find me a quest in the array of objects where singleQuests questTitle equals y
So if we give an example, you can see that questChainId is a 1 so if I pass in a 1 to said function I would get true back, I don't actually care about the object its self, else I would get false.
The same goes for singleQuests, If I pass in hello (case insensitive) I would get back true because there is a quest with the questTitle of 'Hello'. Again I don't care about the object coming back.
The way I would write this is something like:
_.find(theArray, function(questObject){
_.find(questObject.questChains, function(questChain){
if (questChain.questChainId === 1) {
return true;
}
});
});
This is just for the quest chain id comparison. This seems super messy, why? Because I am nesting lodash find, I am also nesting if. It gets a bit difficult to read.
Is this the only way to do this? or is there a better way?
Yeah it can be expressed more simply.
Try something like this:
var exampleArray = [{
questChains: [{
questChainId: 1,
name: 'foo'
}, {
questChainId: 2,
name: 'bar'
}],
singleQuests: [{
questTitle: 'hello world'
}]
}, {
questChains: [{
questChainId: 77,
name: 'kappa'
}]
}];
var result = _.chain(exampleArray)
.pluck('questChains')
.flatten()
.findWhere({ questChainId: 2 })
.value();
console.log('result', result);
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
Using chain and value is optional. They just let you chain together multiple lodash methods more succinctly.
pluck grabs a property from each object in an array and returns a new array of those properties.
flatten takes a nested array structure and flattens it into flat array structure.
findWhere will return the first element which matches the property name/value provided.
Combining all of these results in us fetching all questChain arrays from exampleArray, flattening them into a single array which can be more easily iterated upon, and then performing a search for the desired value.
Case-insensitive matching will be slightly more challenging. You'd either need to either replace findWhere with a method which accepts a matching function (i.e. find) or sanitize your input ahead of time. Either way you're going to need to call toLower, toUpper, or some variant on your names to standardize your search.

Ng-repeat of keys in a nested object

I am trying to use ng-repeat to create list of checkboxes for the keys in a nested object.
My object loosk like:
$scope.kids = [
{
"name": "Will",
"age": 6,
"skills": {
"dancing": false,
"coloring": true
}
},{
"name": "Sally",
"age": 7,
"skills": {
"dancing": false,
"coloring": true,
"runnning": true
}
}
];
and I would like a unique list of the keys in the "skills" object with each skill listed only once, not once for each kid. (i.e. "dancing", "coloring", "running")
This was helpful, but I still can't get a unique list after trying the nested repeats
Here's my current attempt on JSFiddle
Thanks!
Modify Your HTML
I think you need to change your html, remove your ng-repeats with this
<span ng-repeat="kid in kids">
<span ng-repeat="(key,skill) in kid.skills">
<input type="checkbox" ng-modle="{{skill}}"> {{key}}
<br>
</span>
</span>
This should give you a list of boxes connected to the correct $scope and titles.
Here is a link to the modified jsfiddle
Edit
I've modified the code to display a list of all the kids and then within that list we have a list of all of the kids individual skills. Its basically about understanding how loops work.
If I got your question correctly, the best solution for you will be to build a filter that extracts the keys, sorts them and pull unique values. Then you can use it in a single repeat. I forked your jsfiddle and added the filter.
I chose to use lodash to quickly pull out the values, flatten, sort and remove dups. Lodash is a huge playground and you could make the filter even more shorthand.
Since you have a repeating access to this filter, I'd suggest you use memoize to avoid redundant evaluation of the filter. Once it is computed, it is good enough. The basic memoization uses a single key and will not be able to account for other parameters but you can override it and take all params into account and have an accurate and efficient memoization.
The filter is defined like so:
mymodule.filter('uniqueKeys', function(){
return function(input, keyPath){
return _.unique(_.sortBy(_.flatten( _.map(input,
function(item){return _.keys(_.get(item, keyPath));}))));
}
});
and the repeater would be:
<div ng-repeat="skill in kids | uniqueKeys:'skills'">
Note that I pass in the key to find your values, if you had hobbies field that'd you'd like to extract, you just have to write:
<div ng-repeat="skill in kids | uniqueKeys:'hobbies'">

Using a weaker equality test for angular select

Lot's of advice tells me to do this:
// in js
$scope.items = [
{ id: 1, name: 'Foo'},
{ id: 2, name: 'Bar'}];
// in html
<select ng-model="selectedItem"
ng-options="item as item.name for item in items"></select>
And that works fine. This works fine too:
$scope.selectedItem = $scope.items[1];
The select will be initialized to the Bar object.
But this doesn't work:
$scope.selectedItem = { id: 2, name: 'Bar'};
The select control is not initialized to the Bar object (understandably, I think). The selectedItem is equivalent to the Bar object, but not equal to it. I have this problem in an app where parse is the back-end. The selectedItem is a pointer from one object to another, and the items are all of the (handful) of objects in the target class. I get these in two different queries.
Is there a way to manipulate the angular so that I still select an object, but use a custom equality test, like the object id?
Yes, but it will require use of an external library or some scripting of your own. You just need a lookup function which will take your key/value pair (such as you present it in your code sample) and return an item from the array.
The example below uses findWhere in Underscore, which:
Looks through the list and returns the first value that matches all of
the key-value pairs listed in properties.
$scope.selectedItem = _.findWhere($scope.items, {id: 2, name: 'Bar'});
Using .findWhere, you can also search for a subset of the key/value pairs contained in an array item, like so:
$scope.selectedItem = _.findWhere($scope.items, {id: 2});
Demo

Data Binding to a specific item of an array in Angular

Given a data structure that contains an array of JavaScript objects, how can I bind a certain entry from that array to an input field using Angular?
The data structure looks like this:
$scope.data = {
name: 'Foo Bar',
fields: [
{field: "F1", value: "1F"},
{field: "F2", value: "2F"},
{field: "F3", value: "3F"}
]
};
The fields array contains several instances of the given structure, with each entry having both a field attribute and a value attribute.
How can I bind an input control to the value field attribute of the array entry with the field F1?
<input ng-model="???"/>
I know that I could bind all fields using an ng-repeat, but that's not what I want. The above data is just an example from a much larger list of fields, where I only want to bind a pre-defined subset of fields to controls on the screen. The subset is not based on the attributes in the array entries, but is known at design time of the page.
So for the above example, I would try to bind F1 to one input on the page, and F2 to another one. F3 would not be bound to a control.
I've seen examples where a function was used in the ng-model, but it doesn't seem to work with Angular 1.1.0.
Is there another clever way to bind the input field to a specific array entry?
Here's a fiddle that has an example, but does not work since it's trying to use function in the ng-model attribute: http://jsfiddle.net/nwinkler/cbnAU/4/
Update
Based on the recommendation below, this is what it should look like: http://jsfiddle.net/nwinkler/cbnAU/7/
I personally would reorganize the array in a way that field property of an entry of the array become the identifier of the object. Mhhh that sentence may sound strange. What I mean is the following:
$scope.data = {
name: 'F1',
fields: {
F1: {
value: "1F"
},
F2: {
value: "2F"
}
}
};
If you want to bind a the value dynamically and it's an easy and quick way to achieve it.
Here is your fiddle modified so that it words. http://jsfiddle.net/RZFm6/
I hope that helps
You can use an array of objects, just not an array of strings.
HTML:
<div ng-repeat="field in data.fields">
<input ng-model="field.val"/>
</div>
JS:
$scope.data = {
name: 'F1',
fields: [
{ val: "v1" },
{ val: "v2" }
]
};
I've updated #Flek's fiddle here: http://jsfiddle.net/RZFm6/6/
Edit: Sorry just read your question properly, you can still use an array with:
<label>Bound to F1:</label>
<input ng-model="data.fields[0].value"/>
though maybe stop and think. Is there going to be variable number of fields ? or are you making a predetermined number of fields ? Use an array in the former and an object for the latter.
One way to do it is to simply add the necessary references to the scope, like this:
$scope.fieldF1 = fieldValue('F1');
$scope.fieldF2 = fieldValue('F2');
And then use those references:
<input ng-model="fieldF1.value"/>
<input ng-model="fieldF2.value"/>
Fiddle: http://jsfiddle.net/cbnAU/5/
Note: I'm assuming that $scope.data is static, but if it happens to be dynamic you can always watch for changes on it and recalculate the references...

AngularJS - complex filtering based on categories etc

I've done some google-fu but all I can find about AngularJS filters is simple examples about simple filters (mostly on a single field's value).
What I'm after thoguh is somewhat more complex, and I kinda look for help on how to tackle my situation.
Imagine you have an array of the following JSON objects:
{
"id":"1",
"title":"Title",
"categories":[
{"id":"14","name":"DIY"}
],
"topics":[
{"id":"12","name":"Junk Food"}
]
},
{
"id":"2",
"title":"Title 2",
"categories":[
{"id":"4","name":"Test"},
{"id":"14","name":"DIY"},
],
"topics":[
{"id":"2","name":"Food"}
]
}
[...]
so basically each object can have ANY number of "categories" and / or "topics".
Now, my goal is to create a frontend interface that allows me to cumulatively apply various kinds of filters to those JSON objects.
For example, I'd like to say: show only the entries that have category.id = 14 AND topic.id = 2 [etc] and still support deep-linking for the filtered results.
So here's where I'm stuck:
1) what's the best way to use routes for this (ie how would you structure the URLs to support ANY number of filter (based on different values)
2) how should i keep track of the filters added? (ie, how many and which filters have been selected by the user)
Looking at the documentation for the AngularJS filters I'll obviously use the 2nd example for the filtering parameter:
Object: A pattern object can be used to filter specific properties on objects contained by array. For example {name:"M", phone:"1"} predicate will return an array of items which have property name containing "M" and property phone containing "1". A special property name $ can be used (as in {$:"text"}) to accept a match against any property of the object. That's equivalent to the simple substring match with a string as described above.
But I'm not so sure on how to make sure i'm checking the right field (ie topic.id for topics vs category.id for categories)...
Simply put, I'd love to see an example for such a less-trivial filtering scenario.
I think you need something like this instead. See his 'other simple alternative'. I do complex filtering in a service that's injected into my controller, and expose the filtered list on my $scope to the View. I only use Angular filters for relatively simple tasks.
Re: the question about how to expose this on the URL, you'll need some way of representing those filters as strings, and can use $location and $routeParams to populate them into your controller.
This can work if you write a custom filter:
var module = angular.module('app', []);
module.filter("property", ["$filter", function($filter){
var parseString = function(input){
return input.split(".");
}
function getValue(element, propertyArray) {
var value = element;
angular.forEach(propertyArray, function(property) {
value = value[property];
});
return value;
}
return function (array, propertyString, target) {
var properties = parseString(propertyString);
return $filter('filter')(array, function(item){
return getValue(item, properties) == target;
});
}
}]);
HTML part can look like this:
<ul>
<li ng-repeat="data in items | property:'categories.id':<id_of_a_category_we_want>">
{{ data }}
</li>
</ul>
Credit: OnOFF-Switch blog

Categories

Resources