Using a double groupBy in lodash - javascript

So I am trying to categorize an array of objects by a certain attribute. Using groupBy works great the first time. Now I need to loop through those groupings and group them again based on a separate attribute. I am having trouble with this can someone help me out?
TS
this.accountService.getAccountListWithBalance().subscribe(accounts => {
this.accountList = _.groupBy(accounts, 'category');
for (var property in this.accountList) {
if (this.accountList.hasOwnProperty(property)) {
this.accountList.property = _.groupBy(this.accountList.property, 'subcategory');
}
}
generateArray(obj){
return Object.keys(obj).map((key)=>{ return {key:key, value:obj[key]}});
}
HTML:
<ul *ngFor="let item of generateArray(accountList)">
<strong>{{ item.key }}</strong>
<li *ngFor="let i of item.value">{{i.name}}</li>
</ul>
The HTML isnt set for the next level of interation but I know it isnt working if I just console log the resulting object. Like I said its being sorted the first time just not the second time.

I was able to get it to work by just changing my syntax. Using [] instead of . so the working code is as follows.
this.accountService.getAccountListWithBalance().subscribe(accounts => {
this.accountList = _.groupBy(accounts, 'category');
for (var property in this.accountList) {
if (this.accountList.hasOwnProperty(property)) {
this.accountList[property] = _.groupBy(this.accountList[property], 'subcategory');
}
}

I believe that when you are looping through accounts grouped by category, you should try group each category grouped item based on subcategory like this;
this.accountList = _.groupBy(accounts, 'category');
_.foreach(this.accountList, function(categoryAccount) {
_.groupBy(categoryAccount, 'subcategory');
});

Related

Filtering observable array with knockout

Could you please find what I'm doing wrong with this array filter. Fiddle Here
I've been working on it, and making very slow progress. I checked on a lot of samples but not able to find my issue.
Thanks
//This is the part I'm not able to fix
self.filteredPlaces = ko.computed(function() {
var filter = self.filter().toLowerCase();
if (!filter) {
ko.utils.arrayForEach(self.placeList(), function (item) {
});
return self.placeList();
} else {
return ko.utils.arrayFilter(self.placeList(), function(item) {
var result = (item.city().toLowerCase().search(filter) >= 0);
return result;
});
}
});
You did not data-bind filter to any input. You used query instead.
Change your filter value to use the query observable:
var filter = self.query().toLowerCase();
I think I know what you're trying to accomplish so I'll take a shot. There are a few things wrong with this code.
foreach in knockout accepts an array not a function.
http://knockoutjs.com/documentation/foreach-binding.html
I think you're trying to hide entries that don't contain the text in the search box. For that you need the visible binding. I re-factored your code to the sample below.
<div data-bind="foreach: placeList" class="alignTextCenter">
<p href="#" class="whiteFont" data-bind="text: city, visible: isVisible"></p>
</div>
I added isVisible as an item in your array, and an observable in your class.
var initialPlaces = [
{"city":"Real de Asientos","lat":22.2384759,"lng":-102.089015599999,isVisible:true},
{"city":"Todos Santos","lat":23.4463619,"lng":-110.226510099999,isVisible:true},
{"city":"Palizada","lat":18.2545777,"lng":-92.0914798999999,isVisible:true},
{"city":"Parras de la Fuente","lat":25.4492883,"lng":-102.1747077,isVisible:true},
{"city":"Comala","lat":19.3190634,"lng":-103.7549847,isVisible:true},
];
var Place = function(data) {
this.city = ko.observable(data.city);
this.lat = ko.observable(data.lat);
this.lng = ko.observable(data.lng);
this.isVisible = ko.observable(data.isVisible);
};
Lastly, you want to subscribe to the changes of "query" since it's on your text box so that the list updates when the text box changes. It's the self.query.subscribe line in my fiddle. I apologize about the formatting, I tried several times and could not get it to work.
Working fiddle here

Remove item from array using splice

I am using Vue JS, I have 2 different arrays categories and items. Each item can belong to multiple categories, the items are generated dynamically and therefore not initially associated in the category array. Then I parse the category array to create tables containing the different items.
For testing purposes, I attach the items to it's associated category in the mounted vue property, as follows:
mounted: function() {
for (let item of this.items) {
for (let category of item.categories) {
this.categories[category - 1].items.push(item)
}
}
}
Then when the delete button is pressed, I trigger a deleteItem method which uses splice to delete the item from the categories array and from the items array as well, but I am having a little issue there that the correct item does not get deleted.
methods: {
deleteItem: function(item) {
for (let category of item.categories) {
this.categories[category - 1].items.splice(this.categories[category - 1].items.indexOf(item, 1))
}
this.items.splice(this.items.indexOf(item, 1))
}
}
Please see the example Fiddle. Any help will be appreciated.
Change
this.items.splice(this.items.indexOf(item, 1))
to
this.items.splice(this.items.indexOf(item), 1)
so that you pass 1 as second argument to splice.
Note that you do the same error twice.

Replace object with existing value in array in AngularJS

I have an ng-repeat with a select in every item.
The user can select a value (trigging a function that pushes the an object into an array), but they can also change their mind, in which case the code just pushes a second object with the new value, duplicating the first one.
How could I manage to actually delete existing values, leaving only the last one on every ng-change?
Here's my HTML:
<select ng-change="insertproduct(pa.nom, basket)" ng-model="basket">
<option ng-repeat="select in numberofproducts">{{select}}</option>
</select>
And my javascript:
$scope.numberofproducts = [1,2,3,4,5,6,7,8,9,10]
$scope.singleorder = [];
$scope.insertproduct = function(nom, basket){
$scope.numero = {
'producte': nom,
'numero': basket
};
$scope.singleorder.push($scope.numero);
console.log($scope.singleorder);
}
The idea is to create a condition in which if the array contains an object with the parameter ´producte´ equal to the new one, delete the existing and push the new one.
Any tips?
First, use the findIndex method to check if an object with the same property is already in the singleorder array.
function duplicateOrder(order) {
return order.producte === nom;
}
var index = $scope.singleorder.findIndex(duplicateOrder);
Note: browser support for findIndex is limited; it is not supported in Internet Explorer.
Then remove the item with splice:
if(index > -1){
$scope.singleorder.splice(index, 1);
}
You can then push the new one in.
You should also clean up your coding style: don't mix french and english, and use either camelCase or snake_case for your functions to improve readability.
Observation :
Use AngularJS ngOptions attribute instead of ng-repeat.
You can check the index of the element in an array if that was already there you can easily remove previous one.
DEMO
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl',function($scope) {
$scope.numberofproducts = [1,2,3,4,5,6,7,8,9,10]
$scope.newArray = [];
$scope.insertproduct = function(basket) {
var prevIndex = $scope.newArray.indexOf(basket);
if(prevIndex > -1) {
$scope.newArray.splice(prevIndex, 1);
} else {
$scope.newArray.push(basket);
}
console.log($scope.newArray);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="MyCtrl">
<select ng-change="insertproduct(basket)" ng-model="basket" ng-options="select for select in numberofproducts">
</select>
</div>

How do I reverse the order of an array using v-for and orderBy filter in Vue JS?

I am using Vue JS to do viewmodel bindings. In my data object I have an array of items that are sorted in ascending order (oldest to newest) and I'd like to keep it that way for code-based reasons.
var v = new Vue({
el: '#app',
data: {
items: [
{id: 51, message: 'first'},
{id: 265, message: 'second'},
{id: 32, message: 'third'}
],
}
}
However, when I display the array in the template I'd like to reverse the order so that it's descending (newest to oldest). I tried the following:
<ol>
<li v-for="item in items | orderBy -1" track-by="id">
This didn't work since the orderBy filter seems to require a field name as its first argument.
Is there any way to accomplish this in the template using the v-for syntax using the orderBy filter? Or am I going to have to create a custom reverse filter?
Simple and concise solution:
<li v-for="item in items.slice().reverse()">
//do something with item ...
</li>
Instead of reversing the order of the elements for creation, I only change the order of the display.
<ol class="reverseorder">
<li v-for="item in items" track-by="id">
And my CSS
<style>
.reverseorder {
display: flex;
flex-direction: column-reverse;
}
</style>
No need to clone the array and reverse it.
Note: The below works in Vue 1, but in Vue 2 filters are deprecated and you
will see: ' Property or method "reverse" is not defined on the
instance but referenced during render.' See tdom_93's answer for
vue2.
You could create a custom filter to return the items in reversed order:
Vue.filter('reverse', function(value) {
// slice to make a copy of array, then reverse the copy
return value.slice().reverse();
});
Then use it in the v-for expression:
<ol>
<li v-for="item in items | reverse" track-by="id">
https://jsfiddle.net/pespantelis/sgsdm6qc/
Update for Vue2
I want to show some ways that you can work with data and not using filters as they are deprecated in Vue2:
inside computed property
Use computed properties in place of filters, which is much better because you can use that data everywhere in component, not only just in template:
jsFiddle
computed: {
reverseItems() {
return this.items.slice().reverse();
}
}
inside Vuex getter property
If you're using Vuex, and you store your data in store.state object. The best way do some transformation with data stored in state is to do that in getters object (for example filtering through a list of items and counting them, reverse order and so on...)
getters: {
reverseItems: state => {
return state.items.slice().reverse();
}
}
and retrieve state from getters in component computed property:
computed: {
showDialogCancelMatch() {
return this.$store.state.reverseItems;
}
}
Possibly I'm missing some downsides here, but how about iterating over the array from end to start using an index?
<ol>
<li v-for="i in items.length" :set="item = items[items.length - i]">
Like, if your array consists of thousands of elements, copying it with .slice().reverse() every time is probably not the most efficient approach.
Upd.: note, :set is not an official way for defining variables in template, it just works. As an alternative, the item variable could be replaced by a call to some getItem(i) method that would encapsulate the items[items.length - i] expression.
Based on the fact that the directive v-for can accept not only an array but also any other valid JavaScript iterable object (at least in Vue 2.6+ and Vue 3 releases), we can create our own iterable object to loop through a needed array in the opposite direction. I created a very simplified runnable example (for more details - check information about the JavaScript iterator protocol).
class Iterable {
constructor(arr) {
this.arr = arr;
}
*[Symbol.iterator]() {
const arr = this.arr;
for (let i = arr.length - 1; i >= 0; i--) yield arr[i];
}
getIterable(isReversedOrder) {
return isReversedOrder ? this : this.arr;
}
}
Vue.component('list', {
props: ['iterable'],
template: '<ul><li v-for="(el, i) in iterable" :key="`${i}-${el}`">{{ el }}</li></ul>'
});
const app = new Vue({
data() {
return {
todos: new Iterable(['Learn JavaScript', 'Learn Vue', 'Learn Vuex']),
isReversed: true,
inputValue: ''
};
},
computed: {
computedTodos() {
return this.todos.getIterable(this.isReversed);
}
},
methods: {
toggleReverse() {
this.isReversed = !this.isReversed;
},
addTodo() {
this.inputValue && this.todos.arr.push(this.inputValue);
this.inputValue = '';
}
}
});
app.$mount('#app');
<!DOCTYPE html>
<html>
<body style="display: flex; justify-content: center;">
<div id="app">
<button #click="toggleReverse">Toggle reverse to {{ !isReversed }}</button>
<br />
<input v-model="inputValue" style="margin-top:5px;" />
<button #click="addTodo" :disabled="!inputValue">Add todo</button>
<!-- <ul><li v-for="(todo, i) in computedTodos" :key="`${i}-${todo}`">{{ todo }}</li></ul> -->
<list :iterable="computedTodos" />
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.14/dist/vue.js"></script>
<script src="script.js"></script>
</body>
</html>
P.S.Try to avoid using such Array.prototype functions as shift/ unshift , reverse etc. to add / remove items from the beginning of the array or reverse the order, especially in the case when such operations are performed frequently and / or an array includes a big quantity of items, because they are quite costly as for performance (have O(n) complexity).
Another good solution is to use CSS to display elements in the reversed order (see an answer above).
The v-for directive doesn't support iterating backwards, so if you want to order by newest you're going to need to add another field to indicate when the item was added, or change id to increment every time an item is added.
Then, with field being the field indicting the order added:
<li v-for="item in items | orderBy 'field' -1" track-by="id">
For my use case (which is admittedly, apparently different than the OP...) I wanted to have the indices of the Array in reverse order in the v-for "loop."
My solution was to create a Vue app method reverseRange(length) that returns an Array of integers from length-1 to 0. I then used that in my v-for directive and simply referred to my Array elements as myArray[index] every time I needed it.
That way, the indices were in reverse order and I was able to then use them to access elements of the Array.
I hope this helps someone who landed on this page with this subtle nuance in their requirements like me.
You can use lodash reverse:
<li v-for="item in _.reverse(items)">

angularJs exclude already selected items from array

I have an array of objects in $scope.currentSChannels.scgsLink This array of objects have something like
$scope.currentSChannels.scgsLink = [{channelId:1, sCgsLinkId:1, groupNo:1, percentage: 50, expireHrs:4},{channelId:1, sCgsLinkId:2, groupNo:2, percentage:50, expireHrs:1}]
and I also have the following select list
<div class="col-md-4">
<select class="form-control" ng-model="newLink.groupNo"
name="groupNo" id="groupNo"
ng-options="t.value as t.text for t in metaData.spGroups"></select>
</div>
I need to filter that list to not show already selected items in the $scope.currentSChannels.scgsLink groupNo column. I looked at http://christian.fei.ninja/Angular-Filter-already-selected-items-from-ng-options/ and also at AngularJS ng-options to exclude specific object and both seem to be close but not enough as I need to filter against an array and a particular column in that array. How should I implement that filtering?
The template is getting a bit tricky. Assuming selectedLink is the variable that points to the selected groupNo
ng-options="t.value as t.text for t in metaData.spGroups | filter: {value: '!' + currentSChannels.scgsLink[selectedLink].groupNo}"
See this fiddle : the second select contains the same collection as the first one, excluded what is already selected.
Edit: Solution above is for excluding elements according to one value. So as to exclude the elements according to a collection of values, a custom filter would suit best:
Filter
app.filter('channelFilter', function () {
return function (metadata, exclusions) {
var filterFunction = function (metadata) {
// return the metadata object if exclusions array does NOT contain his groupNo
return !exclusions.some(function (exclusion) {
return exclusion.groupNo === metadata.value;
});
};
return metadatas.filter(filterFunction);
};
});
Usage
ng-options="metadata in metadatas | channelFilter: exclusions"
Template
ng-options="t.value as t.text for t in metaData.spGroups | channelFilter: currentSChannels.scgsLink"
Fiddle
That said, would be more efficient to group selected links by groupNo to avoid searches in the array, and filter in the controller.
I wanted to make it a bit more generic, so I've done the following
http://jsfiddle.net/96m4sfu8/
app.filter('excludeFrom', function () {
return function (inputArray, excludeArray, excludeColumnName, inputColumnName) {
if (inputColumnName==undefined)
inputColumnName = 'value';
var filterFunction = function (inputItem) {
return !excludeArray.some(function (excludeItem) {
return excludeItem[excludeColumnName] === inputItem[inputColumnName];
});
};
return inputArray.filter(filterFunction);
};
});

Categories

Resources