Loop through array of objects, hide duplicates, and show number of items? - vuejs2

i am not the best at JavaScript, but have a complex challenge that I am having trouble figuring out.
I need to accomplish the following 3 things:
Loop through an array of objects and display the office names
Show the number of unresolved tickets/issues for each office
I will then use the office id as a param for vue router later ...but we can ignore that one for now as my focus is just on items 1 and 2.
My JSFIDDLE demo:
Here's my HTML template:
<div id="app">
<div>
<table>
<tr>
<th>Office</th>
<th>Number of Unresolved Issues</th>
</tr>
<tr v-for="office in unresolvedIssues" :key="office.issueId">
<td>{{office.office}}</td>
<td></td>
</tr>
</table>
</div>
</div>
And then my Vue code:
new Vue({
el: "#app",
data: {
unresolvedIssues: [
{ issueId: "12345", status: 7, office: "blue" },
{ issueId: "56781", status: 7, office: "orange" },
{ issueId: "23145", status: 7, office: "red" },
{ issueId: "12311", status: 7, office: "blue" },
{ issueId: "33144", status: 7, office: "orange" },
{ issueId: "33244", status: 7, office: "yellow" },
],
offices: [
{ office: 'blue', office_id: 3 },
{ office: 'red', office_id: 1 },
{ office: 'orange', office_id: 2 },
{ office: 'yellow', office_id: 4 },
]
},
methods: {
}
})
In my above code, the v-for gives me the offices, but some offices are listed twice (e.g, "blue" office). How would I filter this array and only show each office once? I thought of trying .reduce() but my attempt did not work. Thanks for anyone that can help!

You were on the right track with reduce. And since you're using Vue, it's best to put it into a computed property as well:
computed: {
unresolvedIssuesGroupedByOffice() {
return this.unresolvedIssues.reduce((groups, issue) => {
let office = groups[issue.office] || []
office.push(issue)
groups[issue.office] = office
return groups
}, {})
}
}
Your updated fiddle: https://jsfiddle.net/7g8zL2nw/

Related

v-for different object properties and accessing array of objects inside it

In my application I am receiving object as below :
{
"data1":[
{},{}{}
],
"data2":[ {},{},{}....],
"data3":[ {},{},{}.....]
}
If someone can help me on how to use v-for here? I want to loop through "data1", "data2"... using v-for. ( in sinlge v-for precisely )
UPDATE:I would like to have object like this.
data :[{
title :"data1",
values: [{ } {} {}]
},
{
title :"data1",
values: [{ } {} {}]
},
.....
]
You can do something like this :
<div id="app">
<h2>Todos:</h2>
<div v-for="t1 in todos.todos1">
<label>{{t1.text}}</label>
</div>
<div v-for="t2 in todos.todos2">
<label>{{t2.text}}</label>
</div>
<div v-for="t3 in todos.todos3">
<label>{{t3.text}}</label>
</div>
</div>
new Vue({
el: "#app",
data: {
todo:{},
todos:{todos1: [
{ text: "Learn JavaScript 1", done: false },
{ text: "Learn Vue 1", done: false }
],
todos2: [
{ text: "Play around in JSFiddle 2", done: true },
{ text: "Build something awesome 2", done: true }
],
todos3: [
{ text: "Learn Vue 3", done: false },
{ text: "Play around in JSFiddle 3", done: true },
]
}
},
created(){
this.todo = Object.values(this.todos)
console.log(this.todo)
}
})
You can do something like
<div v-for="(value, propertyName, index) in items"></div>
WARNING
When iterating over an object, the order is based on the enumeration order of Object.keys(), which is not guaranteed to be consistent across JavaScript engine implementations.
The above can be found on the Vue Documentation.

Display an object added to an array in a child component in Vue

In vue I have a component that displays the content of an array in its parent App.vue.
If I push to the array, ListofLists how do I get the display list component to display the updated data?
I have a form in a child component that $emits a javascript object newList which is captured by the method addList. I expected Vue to detect a change to the ListofList and recompute the value of loadedList. If I am right to expect this how do I get Vue to display the updated list?
Should I use computed or watch? Is it necessary to use forceRerender or does the problem lie elsewhere.
What I have tried
<ShowListComponent>
:key="componentKey"
v-bind:loadedList="loadedList"/>
computed: {
loadedList: function() {
return this.ListofLists[this.currentList];
}
},
methods: {
changeList: function(id) {
this.currentList = id;
this.ListofLists[this.currentPlaylist];
console.log('changed List' + this.ListofLists[this.currentList]);
},
addList: function(newList) {
this.ListofLists.push( newList );
console.log('lists after push ' +this.ListofLists);
this.forceRerender();
},
What I see
The console.log shows the ListofLists is updated correctly.
I get an error:
[Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
What I expect
The new list being displayed correctly.
EDIT - The template and component used
<template>
<div id="app">
<MediaPlayer
:key="componentKey"
v-on:openAllSongs="showAllSongs = !showAllSongs"
v-on:openPlaylist="showPlaylist= !showPlaylist"
v-bind:loadedPlaylist="loadedPlaylist"/>
<AllSongs
v-on:closeAllSongs="showAllSongs = false"
v-bind:songs="playlists[0].songs"
v-bind:class="{show : showAllSongs}"
/>
<Playlists
v-on:closePlaylist="showPlaylist = false"
v-bind:playlists="playlists"
v-bind:class="{show : showPlaylist}"
v-on:change-playlist="changePlaylist"
v-on:add-playlist="addPlaylist"
/>
</div>
</template>
<script>
import MediaPlayer from './components/MediaPlayer.vue'
import Playlists from "./components/Playlists.vue";
export default {
name: 'App',
components: {
MediaPlayer,
AllSongs,
Playlists,
},
props: ['playlist'],
data() {
return {
playlists: [
{
id: 1,
name: 'All Songs',
description: 'Magnum Opus',
created: '18/09/1830',
updated: '23/04/2020',
created_by: 'Jamie',
songs: [
{
id: '1',
position_in_list_index: 47,
name: 'i dont know what this is but i like it',
filename: 'i dont know what this is but i like it.mp3',
length_in_seconds: 200,
startdate: 'yesterday',
tags: 'quirky, instumental'
},
{
id:"3",
position_in_list_index: 38,
name: "sunday junkie",
filename: "sunday junkie.mp3",
length_in_seconds: 222,
tags: 'indie'
},
{
id: 4,
position_in_list_index: 40,
name: "burned",
filename: "burned.mp3",
length_in_seconds: 303,
tags: 'melancholy, serious'
},
{
id: 5,
position_in_list_index: 46,
name: "losing limbs like theres no tomorrow",
filename: "losing limbs like theres no tomorrow.mp3",
length_in_seconds: 232,
tags: 'hiphop, dark, synth'
}
],
},
{
id:3,
name: 'Rock Hard Seduction',
description: 'songs that bring a tear to your eye',
created: '21/02/2020',
updated: '23/03/2020',
created_by: 'Dewi',
songs: [
{
id: 4,
position_in_list_index: 21,
name: "trefnant rainbow v2",
filename: "trefnant rainbow v2.mp3",
length_in_seconds: 174,
tags: "rock"
},
{
id: 9,
position_in_list_index: 61,
name: "trefnant wind added solo",
filename: "trefnant wind added solo.mp3",
length_in_seconds: 268,
tags: 'folk'
}
]
},
{
id:4,
name: 'Family friendly',
description: 'they got no lyrics',
created: '20/02/2020',
updated: '23/02/2020',
created_by: 'Dewi',
songs: [
{
id: "15",
position_in_list_index: 74,
name: "dont waltz with a psycho like me",
filename: "dont waltz with a psycho like me.mp3",
length_in_seconds: 273
},
{
id: "17",
position_in_list_index: 76,
name: "lie down on the tracks",
filename: "lie down on the tracks.mp3",
length_in_seconds: 225,
tags: 'rock, instrumental, sounds like someone else'
},
{
id: "28",
position_in_list_index: 87,
name: "you two frozen heroes",
filename: "you two frozen heroes.mp3",
length_in_seconds: 267
}
]
},
],
showPlaylist: false,
currentPlaylist: 0,
componentKey: 0
}
},
computed: {
loadedPlaylist: function() {
console.log('playlist length in loadedPlaylist ' + this.playlists.length);
console.log('current playlist id '+ this.currentPlaylist);
return this.playlists[this.currentPlaylist];
}
},
methods: {
changePlaylist: function(id) {
this.currentPlaylist = id - 1;
this.playlists[this.currentPlaylist];
console.log('changed playlist' + this.playlists[this.currentPlaylist]);
},
addPlaylist: function(newPlaylist) {
this.playlists.push( newPlaylist );
console.log('playlists after push ' +this.playlists);
this.forceRerender();
this.changePlaylist(newPlaylist.id);
},
forceRerender: function() {
this.componentKey++;
console.log("componentKey " + this.componentKey);
}
}
}
</script>
the component template I am using to display the list.
<template>
<div class="media-player">
<draggable
v-model="loadedPlaylist.songs" class="songlist"
:group="{ name: 'songs'}"
ghost-class="ghost"
v-bind:key="loadedPlaylist.id"
>
<div
class="songlist-item"
#click='loadTrack(index, true)'
v-bind:class="{ 'is-active': index === activeTrack }"
v-bind:key="song.id"
v-for="(song, index) in loadedPlaylist.songs"
>
<h3>{{index + 1}}. {{song.name}}<span></h3>
</div>
</draggable>
</div>
</template>
My code was adding a new object to the array correctly and the update was passed to the child component correctly.
I was getting an error because the new object contained an empty array. trying to access it directly caused an error. Adding
<h2 v-if="loadedPlaylist.songs.length">{{loadedPlaylist.songs[activeTrack].name}}</h2>
<h2 v-else>Add Songs below</h2>
fixed the error.

VueJS - Auto create a A-Z letters list from the data

Is it possible to create a A-Z letters list (like this) from the data from a API and Vue to be able to determine if a property in a data contains a name that starts with what letter. If the data doesn't contain a specific letter name then remove/disable the href attribute from the letter anchor tag.
In the linked example, letters K, X and Z are missing coz they don't have the data
JSON
[
{
"id": 77,
"link": "http://my-site/cosmoquotes/authors/anonymous/",
"name": "Anonymous",
"slug": "anonymous"
},
{
"id": 72,
"link": "http://my-site/authors/ferdinand-marcos/",
"name": "Ferdinand Marcos",
"slug": "ferdinand-marcos"
},
{
"id": 75,
"link": "http://my-site/authors/john-f-kennedy/",
"name": "John F. Kennedy",
"slug": "john-f-kennedy"
},
{
"id": 67,
"link": "http://my-site/authors/john-maxwell/",
"name": "John Maxwell",
"slug": "john-maxwell"
}
]
Component
export default {
data() {
return {
authorsRequest: {
type: 'authors',
params: {
per_page: 100
}
},
}
},
computed: {
authors () {
return this.$store.getters.requestedItems(this.authorsRequest)
},
},
methods: {
getAuthors() {
return this.$store.dispatch('getItems', this.authorsRequest)
},
},
created() {
this.getAuthors()
}
}
So as per the returned data, only the letters 'A', 'F' and 'J' should be clickable/displayed.
I managed to do it like this,
unfortunatly it needs the authors array and the conditionnal function to be outside of the Vue component because I couldn't find how to pass argument to computed values
But since I'm new to vue (didn't even finish reading the introduction) I'm sure there has to be a better solution
EDIT: found the way to have the function in the component with methods, I could then move the data in the component too
let a = new Vue({
el: "#selector",
data: {
authors: [{"id": 77,"link": "http://my-site/cosmoquotes/authors/anonymous/","name": "Anonymous","slug": "anonymous"},{"id": 72,"link": "http://my-site/authors/ferdinand-marcos/","name": "Ferdinand Marcos","slug": "ferdinand-marcos"},{"id": 75,"link": "http://my-site/authors/john-f-kennedy/","name": "John F. Kennedy","slug": "john-f-kennedy"},{"id": 67,"link": "http://my-site/authors/john-maxwell/","name": "John Maxwell","slug": "john-maxwell"}]
},
computed: {
// there have to be a way to get this array without doing it like this but I don't know it ^^
letters() {
let letters = []
for(let i = "A".charCodeAt(0); i <= "Z".charCodeAt(0); i++) {letters.push(String.fromCharCode([i]))}
return letters
}
},
methods: {
// you may add a toUpperCase()/toLowerCase() if you're not sure of the capitalisation of you datas
isALink(letter) {
return this.authors.some(aut => aut.name.startsWith(letter))
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="selector">
<template v-for="letter in letters">
<a v-if="isALink(letter)" :href="letter">{{ letter }}</a>
<a v-else>{{ letter }}</a>
</template>
</div>
you can set the unique name as the id of dom. when you click to letter X, just get the first name start with X , and use getElementById to match the dom and scroll to the dom.

Vue Component Not Displaying Nested Div

I have a component that works fine. However, when I appended <div :id="hint"></div> to the component this specific DIV isn't rendering. I'm sure it has something to do with the way I'm using a DIV to reference the template in my HTML but I don't know how to refine it.
<div
ref="boxAnswers"
is="box-answers"
v-for="box in boxes.slice().reverse()"
v-bind:key="box.id"
v-bind:level="box.level"
v-bind:hint="box.hint"
></div>
Vue.component('box-answers', {
props: ['level','hint'],
template: '<div class="droppable answer ui-widget-header" :id="level"></div><div :id="hint"></div>'
});
new Vue({
el: '#mainapp',
data: {
boxes: [
{ id: 1, level: 'baselevel-1', hint: 'hint-1' },
{ id: 2, level: 'baselevel-2', hint: 'hint-2' },
{ id: 3, level: 'baselevel-3', hint: 'hint-3' },
{ id: 4, level: 'baselevel-4', hint: 'hint-4' },
{ id: 5, level: 'baselevel-5', hint: 'hint-5' }
]
}
});

VueTwo Way Data Binding with Nested Components

Suppose I want to display a List of Questions. For each question, there is a list of answers, none of which are right or wrong. For each question, the user can choose an answer. I'm wondering how to create two-way binding on the selected answer.
The Vue:
new Vue(
{
el: "#app",
data:
{
questions: [{}]
}
}
Example Question Model:
{
id: 1,
name: "Which color is your favorite?",
selectedAnswerId: null,
selectedAnswerName: null,
answers:
[
{id: 1, name: red, photoUrl: ".../red", selected: false},
{id: 2, name: green, photoUrl: ".../green", selected: false},
{id: 3, name: blue, photoUrl: ".../blue", selected: false},
]
}
Components:
var myAnswer =
{
props: ["id", "name", "url", "selected"],
template:
`
<div class="answer" v-bind:class="{selected: selected}">
<img class="answer-photo" v-bind:src="url">
<div class="answer-name">{{name}}</div>
</div>
`
};
Vue.component("my-question",
{
props: ["id", "name", "answers"],
components:
{
"my-answer": myAnswer
},
template:
`
<div class ="question">
<div class="question-name">{{name}}</div>
<div class="question-answers">
<my-answer v-for="answer in answers" v-bind:id="answer.id" v-bind:name="answer.name" v-bind:url="answer.photoUrl" v-bind:selected="answer.selected"></my-answer>
</div>
</div>
`
});
When the user selects an answer to a question by clicking on the div, I want the Question model's selectedAnswerId/selectedAnswerName along with the answers selected property to be set accordingly. Therefore, what do I need to add to my components in order to accomplish this two-way binding? I believe it requires input elements and v-model, but I couldn't quite figure it out. Also, I am only one day into Vue.js and have no experience with related frameworks. So if I am doing anything blatantly wrong or against best practice, that would be good to know as well. Thanks in advance!
The answer will handle a click event and emit a (custom) selected-answer event. The question will have its own data item to store the selected answer ID; the answer component's selected prop will be based on that. The question will handle the selected-answer event by setting its selectedId.
var myAnswer = {
props: ["id", "name", "url", "selected"],
template: `
<div class="answer" v-bind:class="{selected: selected}"
#click="setSelection()"
>
<img class="answer-photo" :src="url">
<div class="answer-name">{{name}}</div>
</div>
`,
methods: {
setSelection() {
this.$emit('selected-answer', this.id);
}
}
};
Vue.component("my-question", {
props: ["id", "name", "answers"],
data() {
return {
selectedId: null
};
},
components: {
"my-answer": myAnswer
},
template: `
<div class ="question">
<div class="question-name">{{name}}</div>
<div class="question-answers">
<my-answer v-for="answer in answers"
:id="answer.id" :name="answer.name" :url="answer.photoUrl"
:selected="answer.id === selectedId" #selected-answer="selectAnswer"></my-answer>
</div>
</div>
`,
methods: {
selectAnswer(answerId) {
this.selectedId = answerId;
}
}
});
new Vue({
el: '#app',
data: {
questions: [{
id: 1,
name: "Which color is your favorite?",
answers: [{
id: 1,
name: 'red',
photoUrl: ".../red"
},
{
id: 2,
name: 'green',
photoUrl: ".../green"
},
{
id: 3,
name: 'blue',
photoUrl: ".../blue"
},
]
}]
}
});
.answer {
cursor: pointer;
}
.selected {
background-color: #f0f0f0;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<div id="app">
<my-question v-for="q in questions" :name="q.name" :answers="q.answers"></my-question>
</div>