Vue.js re-rendering sorted array - arrays

I have a basic sorting UI to sort comments based on some values:
Part of CommentsSection template:
<div v-if="numComments" class="tabs d-flex">
<span class="text-muted">Sort by:</span>
<div class="tab" :class="{active: tab === 0}" #click="sortComments(0)">Rating</div>
<div class="tab" :class="{active: tab === 1}" #click="sortComments(1)">Newest</div>
<div class="tab" :class="{active: tab === 2}" #click="sortComments(2)">Oldest</div>
</div>
<ul v-if="numComments" class="comments-list">
<li is="comment" #delete="numComments -= 1" v-for="comment in sortedComments" :data="comment"></li>
</ul>
CommentsSection:
export default {
name: 'comments-section',
components: {
CommentForm,
Comment
},
props: ['comments', 'submissionId'],
data() {
return {
tab: 0,
numComments: this.comments.length,
sortedComments: this.comments.slice()
}
},
created() {
this.sortComments();
},
methods: {
sortComments(type = 0) {
this.tab = type;
if (type === 0) {
this.sortedComments.sort((a, b) => b.rating - a.rating);
} else if (type === 1) {
this.sortedComments.sort((a, b) => moment(b.create_time).unix() - moment(a.create_time).unix());
} else {
this.sortedComments.sort((a, b) => moment(a.create_time).unix() - moment(b.create_time).unix());
}
},
...
}
...
}
CommentSingle (component being rendered in list):
export default {
name: 'comment-single',
props: ['data'],
data() {
return {
agree: this.data.rated === 1,
disagree: this.data.rated === -1
}
}
...
}
The CommentSingle template is not being re-rendered so agree and disagree don't update. But the actual list does render the proper sort when clicking each sorting tab, but each comment in the list has the wrong agree and disagree (the original sorted array's values). Any idea how to fix this?

Solved by binding a key to the rendered component:
<li is="comment" #delete="numComments -= 1" v-for="comment in sortedComments" :key="comment.id" :data="comment"></li>
Reference: https://v2.vuejs.org/v2/guide/list.html#key

Related

Nested *ngFors - better alternative? (Angular 7)

I have an object which contains an array - this array can contain one or more objects of the same type as the parent one. There is no boundary on the amount of levels possible. To display all data in the current data set I have this code:
<div *ngIf="selectedUserMappings">
<ul *ngFor="let group of selectedUserMappings.childGroups">
<li style="font-weight:600;">{{group.name}}</li>
<div *ngFor="let child of group.childGroups">
<li *ngIf="child.active">{{child.name}}</li>
<div *ngFor="let n2child of child.childGroups">
<li *ngIf="n2child.active">{{n2child.name}}</li>
<div *ngFor="let n3child of n2child.childGroups">
<li *ngIf="n3child.active">{{n3child.name}}</li>
</div>
</div>
</div>
</ul>
</div>
Which is not a very good way to achieve the wanted result. Any pointers on a better approach?
Edit: From the suggestions so far, I think the way to go will be a dummy component. I need to show the outer parents with different styling in the list, which I will be able to now. But, I also need to hide top-level parents where no childs are active (all the objects have an active-boolean), as well as childs that are not active. The children of non-active childs should still be visible though. Any ideas? This is what I've got so far:
import { Component, OnInit, Input } from '#angular/core';
import { UserGroupMapping } from 'src/app/models/models';
#Component({
selector: 'list-item',
templateUrl: './list-item.component.html',
styleUrls: ['./list-item.component.scss']
})
export class ListItemComponent implements OnInit {
#Input() data;
list: Array<any> = [];
constructor() { }
ngOnInit() {
this.data.forEach(item => {
this.createList(item, true);
});
}
createList(data, parent?) {
if (parent) {
this.list.push({'name': data.name, 'class': 'parent'});
if (data.childGroups && data.childGroups.length > 0) {
this.createList(data, false);
}
} else {
data.childGroups.forEach(i => {
this.list.push({'name': i.name, 'class': 'child'});
if (i.childGroups && i.childGroups.length > 0) {
this.createList(i, false);
}
});
}
console.log(this.list);
}
}
Called from the parent component like this:
<div *ngIf="mappings">
<list-item [data]="mappings.childGroups"></list-item>
</div>
You describing a tree. Try using some sort of tree component. For example prime ng has a tree component.
I ended up scrapping the dummy component, building the lists directly in my component before showing it - so it will be easier to display changes immediately.
createSummary() {
this.listFinished = false;
this.mappings.childGroups.forEach(item => {
this.list = [];
this.createList(item, true);
this.list.forEach(i => {
if (i.active) {
this.list[0].active = true;
}
});
this.lists.push(this.list);
});
this.listFinished = true;
}
createList(data, parent?) {
if (parent) {
this.list.push({'name': data.name, 'class': 'parent', 'active': false});
if (data.childGroups && data.childGroups.length > 0) {
this.createList(data, false);
}
} else {
data.childGroups.forEach(i => {
this.list.push({'name': i.name, 'class': 'child', 'active': i.active});
if (i.childGroups && i.childGroups.length > 0) {
this.createList(i, false);
}
});
}
}
Then I show it like this:
<div *ngIf="listFinished">
<ul *ngFor="let list of lists">
<div *ngFor="let item of list">
<li [class]="item.class" *ngIf="item.active">
{{item.name}}
</li>
</div>
</ul>
</div>

Vuejs checkbox indeterminate status

I need little help. I have world regions list with countries inside:
{
'North American Countries' : {
'countries' : {
'us' : { 'name' : 'United States' } ,
'ca' : { 'name': 'Canada' }
.
.
.
.
}
},
'European Countries' : {
......
}
}
HTML:
<ul v-for="(regionName, region) in regions">
<li>
<label>{{ regionName }}</label>
<input type="checkbox" #change="toggleGroupActivation(regionName)">
</li>
<li v-for="country in region.countries">
<div>
<label for="country-{{ country.code }}">{{ country.name }}</label>
<input id="country-{{ country.code }}" type="checkbox" :disabled="!country.available" v-model="country.activated" #change="toggleCountryActivation(regionName, country)">
</div>
</li>
</ul>
And I try to build the list with checkboxes, where you can select countries. If check whole region's checkbox, automatically checked all countries in it region. If are checked only few countries in region(not all), need to display indeterminate checkbox status by region checkbox. How to handle it?
The usual solution to the Select All checkbox is to use a computed with a setter. When the box is checked, all the sub-boxes are checked (via the set function). When a sub-box changes, the Select All box value is re-evaluated (in the get function).
Here, we have a twist: if the sub-boxes are mixed, the Select All box should indicate that somehow. The approach is still to use a computed, but instead of just true and false values, it can return a third value.
There's no built-in way of representing a third value in a checkbox; I've chosen to replace it with a yin-yang emoji.
const rawData = {
'North American Countries': {
'countries': {
'us': {
'name': 'United States'
},
'ca': {
'name': 'Canada'
}
}
},
'European Countries': {
countries: {}
}
};
const countryComponent = Vue.extend({
template: '#country-template',
props: ['country', 'activated'],
data: () => ({ available: true })
});
const regionComponent = Vue.extend({
template: '#region-template',
props: ['region-name', 'region'],
data: function () {
const result = {
countriesActivated: {}
};
for (const c of Object.keys(this.region.countries)) {
result.countriesActivated[c] = { activated: true };
}
return result;
},
components: {
'country-c': countryComponent
},
computed: {
activated: {
get: function() {
let trueCount = 0;
let falseCount = 0;
for (const cName of Object.keys(this.countriesActivated)) {
if (this.countriesActivated[cName]) {
++trueCount;
} else {
++falseCount;
}
}
if (trueCount === 0) {
return false;
}
if (falseCount === 0) {
return true;
}
return 'mixed';
},
set: function(newValue) {
for (const cName of Object.keys(this.countriesActivated)) {
this.countriesActivated[cName] = newValue;
}
}
}
}
});
new Vue({
el: 'body',
data: {
regions: rawData
},
components: {
'region-c': regionComponent
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<template id="region-template">
<li>
<label>{{ regionName }}</label>
<input v-if="activated !== 'mixed'" type="checkbox" v-model="activated">
<span v-else>☯</span>
<ul>
<country-c v-for="(countryName, country) in region.countries" :country="country" :activated.sync="countriesActivated[countryName]"></country-c>
</ul>
</li>
</template>
<template id="country-template">
<li>
<label for="country-{{ country.code }}">{{ country.name }}</label>
<input id="country-{{ country.code }}" type="checkbox" :disabled="!available" v-model="activated">
</li>
</template>
<ul>
<region-c v-for="(regionName, region) in regions" :region-name="regionName" :region="region" :countriesActivated=""></region-c>
</ul>

Check length of an array of objects for a specific value in ng-if

I'm passing an array of objects that looks like this:
"articles": [
{
"_id": "1234",
"type": "location",
"content": {}
},
{
"_id": "1235",
"type": "event",
"content": {}
}, ...
Then I use a ng-repeat to loop over this array where I filter by event:
<div ng-if="articles.type == event && articles.length > 0">
<h3>Events</h3>
<ul>
<li ng-repeat="article in articles | filter: { type: 'event'} track by $index">
<h2>{{article.content.title}}</h2>
<p>{{article.content.intro}}</p>
</li>
</ul>
</div>
<div ng-if="articles.type == location && articles.length > 0">
<h3>Hotspots</h3>
<ul>
<li ng-repeat="article in articles | filter: { type: 'location'} track by $index">
<h2>{{article.content.title}}</h2>
<p>{{article.content.intro}}</p>
</li>
</ul>
</div>
The behaviour I'm trying to achieve is if there are no articles with type event or location than I'm not showing them.
My question is how do I check this in my ng-if because now I'm checking the whole array's length instead of the array's length for that type of article.
Maybe something like this:
$scope.article_type = function(type) {
for (i = 0; i < $scope.articles.length; i += 1) {
if ($scope.articles[i].type === type) {
return true;
}
}
return false;
}
Then in your HTML:
<div ng-show="article_type('event')">
EDIT: Anik's answer is more optimal
Create a method in scope
$scope.isExists=function(type){
var obj = $scope.articles.find(function(x){ return x.type == type ; });
return obj !== null;
}
Then try like this
in html
<div ng-if="isExists('event')">
and
<div ng-if="isExists('location')" >

Angular-xeditable: Need a checklist that displays checked items

I would like to use a check list and show the user the boxes she has checked.
I am using this framework: http://vitalets.github.io/angular-xeditable/#checklist . See his example 'Checklist' versus his example 'Select multiple'. However, I do not want to display a link with a comma separated string, i.e., join(', '). I would like each selection to appear beneath the previous, in an ordered list or similar.
Pretty much copied from his examples, here are the guts of my controller:
$scope.userFeeds = {
feeds: {}
};
$scope.feedSource = [
{ id: 1, value: 'All MD' },
{ id: 2, value: 'All DE' },
{ id: 3, value: 'All DC' }
];
$scope.updateFeed = function (feedSource, option) {
$scope.userFeeds.feeds = [];
angular.forEach(option, function (v) {
var feedObj = $filter('filter')($scope.feedSource, { id: v });
$scope.userFeeds.feeds.push(feedObj[0]);
});
return $scope.userFeeds.feeds.length ? '' : 'Not set';
};
And here is my html:
<div ng-show="eventsForm.$visible"><h4>Select one or more feeds</h4>
<span editable-select="feedSource"
e-multiple
e-ng-options="feed.id as feed.value for feed in feedSource"
onbeforesave="updateFeed(feedSource, $data)">
</span>
</div>
<div ng-show="!eventsForm.$visible"><h4>Selected Source Feed(s)</h4>
<ul>
<li ng-repeat="feed in userFeeds.feeds">
{{ feed.value || 'empty' }}
</li>
<div ng-hide="userFeeds.feeds.length">No items found</div>
</ul>
</div>
My problem is - display works with editable-select and e-multiple, but not with editable-checklist. Swap it out and nothing is returned.
To workaround, I have tried dynamic html as in here With ng-bind-html-unsafe removed, how do I inject HTML? but I have considerable difficulties getting the page to react to a changed scope.
My goal is to allow a user to select from a checklist and then to display the checked items.
Try this fiddle: http://jsfiddle.net/mr0rotnv/15/
Your onbeforesave will need to return false, instead of empty string, to stop conflict with the model update from xEditable. (Example has onbeforesave and model binding working on the same variable)
return $scope.userFeeds.feeds.length ? false : 'Not set';
If you require to start in edit mode add the attribute shown="true" to the surrounding form element.
Code for completeness:
Controller:
$scope.userFeeds = {
feeds: []
};
$scope.feedSource = [
{ id: 1, value: 'All MD' },
{ id: 2, value: 'All DE' },
{ id: 3, value: 'All DC' }
];
$scope.updateFeed = function (feedSource, option) {
$scope.userFeeds.feeds = [];
angular.forEach(option, function (v) {
var feedObj = $filter('filter')($scope.feedSource, { id: v });
if (feedObj.length) { // stop nulls being added.
$scope.userFeeds.feeds.push(feedObj[0]);
}
});
return $scope.userFeeds.feeds.length ? false : 'Not set';
};
Html:
<div ng-show="editableForm.$visible">
<h4>Select one or more feeds</h4>
<span editable-checklist="feedSource"
e-ng-options="feed.id as feed.value for feed in feedSource"
onbeforesave="updateFeed(feedSource, $data)">
</span>
</div>
<div ng-show="!editableForm.$visible">
<h4>Selected Source Feed(s)</h4>
<ul>
<li ng-repeat="feed in userFeeds.feeds">{{ feed.value || 'empty' }}</li>
<div ng-hide="userFeeds.feeds.length">No items found</div>
</ul>
</div>
Css:
(Used to give the "edit view" a list appearance)
.editable-input label {display:block;}
Also there is the option of using a filter if you do not need to do any validation or start in edit mode.
Controller:
$scope.user = { status: [2, 3] };
$scope.statuses = [
{ value: 1, text: 'status1' },
{ value: 2, text: 'status2' },
{ value: 3, text: 'status3' }
];
$scope.filterStatus = function (obj) {
return $scope.user.status.indexOf(obj.value) > -1;
};
HTML:
<a href="#" editable-checklist="user.status" e-ng-options="s.value as s.text for s in statuses">
<ol>
<li ng-repeat="s in statuses | filter: filterStatus">{{ s.text }}</li>
</ol>
</a>

i want hide only specific list element

i am new to angularjs. i have created list using ng-repeat. just i want to hide the selected list element from list:
html code which i prefered:
<ul>
<li ng-repeat="profile in profileMenu">
<div class="hederMenu" ng-hide="configureDisplay" ng-click="setProfile(profile.name)">
<a class="anchor" style="width:100%" >{{profile.name}}</a>
</div>
</li>
</ul>
here is controller code
$scope.profileMenu = [{
name : "My Profile"
}, {
name : "Configure"
}, {
name : "Logout"
}
];
$scope.profile = "";
$scope.setProfile = function (test) {
$scope.profileSelected = test;
if ($scope.profileSelected == "Configure") {
$location.path("/home/configure"); // if user click configure then this element will hide
$scope.configureDisplay = true;
}
if ($scope.profileSelected == "My Profile") {
$location.path("/home/dashboard");
$scope.configureDisplay = false;
}
if ($scope.profileSelected == "Logout") {
window.location.assign("http://mitesh.demoilab.pune/")
}
return $scope.profileSelected = test;
}
You have to set the configureDisplay property on the actual "Configure" profile item. Not sure what you're doing with the selection list, but I assume you would want the "Configure" item visible again when selecting another item. Therefore you'll also have to reset the "Configure" item back to false when selecting another item.
I modified your example a bit. Notice instead of passing the profile.name on setProfile, i'm passing the profile object. This just simplifies the interaction.
<ul>
<li ng-repeat="profile in profileMenu">
<div class="hederMenu" ng-hide="profile.configureDisplay" ng-click="setProfile(profile)">
<a class="anchor" style="width:100%" >{{profile.name}}</a>
</div>
</li>
</ul>
$scope.setProfile = function (selectedProfile) {
//reset the items
for (var i in $scope.profileMenu) {
$scope.profileMenu[i].configureDisplay = false;
}
if (selectedProfile.name == "Configure") {
$location.path("/home/configure"); // if user click configure then this element will hide
selectedProfile.configureDisplay = true;
}
if (selectedProfile.name == "My Profile") {
$location.path("/home/dashboard");
}
if (selectedProfile.name == "Logout") {
window.location.assign("http://mitesh.demoilab.pune/")
}
return true;
}
you need to do few changes ,
<li ng-repeat="profile in profileMenu">
<div class="hederMenu" ng-hide="profile.configureDisplay" ng-click="setProfile(profile)">
<a class="anchor" style="width:100%" >{{profile.name}}</a>
</div>
</li>
And in controler,
$scope.setProfile = function (test) {
$scope.profileSelected = test.name;
if ($scope.profileSelected == "Configure") {
$location.path("/home/configure");
test.configureDisplay = true;
}
if ($scope.profileSelected == "My Profile") {
$location.path("/home/dashboard");
test.configureDisplay = false;
}
if ($scope.profileSelected == "Logout") {
window.location.assign("http://mitesh.demoilab.pune/")
}
return test;
}

Resources