using v-for with multiple keys [closed] - vue.js

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 6 months ago.
Improve this question
So I am going to be honest I am NOT sure if using multiple keys is the right solution but in my head it is what makes sense. Here is the task at hand.
I have 3 objects I am showing in a v-for statement:
The bottom item (CHILD OF 1) should be located under PARENT 1. Now, in my code, I have the following
<div class="text-white" v-for="comment in comments" :key="comment.id">
Now, there is another field in the object called comment.parent_id That relates back to the original comment.id
How would I go about properly sorting this so that I can format the comments accordingly. (side note not that it matters, but, ill be indenting the child comment which is why this is important).
Thanks!

Solution for only one depth
I would organize the data differently and use a different approach.
The data would be more coherent if you have an object list with your parent comments and your parent object has a child comment object list.
Example:
comments = [{
id: 1,
username: "John SMITH",
timestamp: "9999-12-31 23:59:59.999999",
text: "This is a parent comment",
commentChild: [{
id: 2,
username: "Angela SMITH",
timestamp: "9999-12-31 23:59:59.999999",
text: "This is a Child comment",
},]
},]
With this it is easy to make a first for loop to browse your parents and within it, a for loop to browse the children, if there are any.
<template>
<div>
<template v-for="comment in comments" :key="comment.id">
<div>Your parent comment data</div>
<template v-for="commentChild in comment.commentChild" :key="commentChild.id">
<div>Your child comment data</div>
</template>
</template>
</div>
</template>
Solution for infinite depth
To handle an unknown depth, you must have a recursive component and a parent component that will initiate the first call.
This is what it can do:
Comment Interface
interface Comment {
id: number;
comments?: Comment[];
// And all data you want
}
Component Parent (initial call):
<template>
//Only here for call the recursive component
// Take your list of comments
<MyCommentsRecursiveComponent :comments="comments"></Test>
</template>
MyCommentsRecursiveComponent:
<script setup lang="ts">
defineProps<{ comments: Comment[] }>();
</script>
<template>
<div>
<template v-for="(actualComment, index) in comments">
<p>{{ actualComment.id }}</p>
<div>
// Component call himself / Recursive
<MyCommentsRecursiveComponent
v-if="actualComment.comments"
:comments="actualComment.comments"
></MyCommentsRecursiveComponent>
</div>
</template>
</div>
</template>

Related

Vue changes to array not updating the DOM

I'm trying to make changes to an array in the parent component (insert and update data) that is passed to the child component via Props, but the DOM is not being updated.
Parent component:
<UsersList
v-for="(role, i) in userRolesNames"
:key="i"
:users="usersPages[i].data"
/>
Child component:
<template>
<div
v-for="user in users"
:key="user.id"
>
<span>{{ user.name }}</span>
<div>
<i
class="bi bi-pen-fill edit-icon m-pointer"
#click="onClickEdit(user)"
></i>
<i
class="bi bi-trash-fill delete-icon ms-2 m-pointer"
#click="onClickDelete(user)"
></i>
</div>
</div>
</template>
<script lang="ts">
// recieving the array
props: {
users: {
required: true,
type: Array as PropType<usersType[]>
}
}
</script>
Each user basically has the following structure:
{
id: x,
name: 'x',
email: 'x',
login: 'x',
role: x
}
The problem is that when trying to insert or update a record in the usersPages[i].data array, the DOM doesn't change. If I use the Vue developer tools, the data is changing correctly, but the DOM isn't.
Tried inserting using the push method on the array but without success. The only thing that worked was this:
const newUser = response.data;
const page = this.usersPages[newUser.role - 1];
Vue.set(page, 'data', [...page.data, newUser]);
To update I tried to directly change the user properties, but like the insert the DOM doesn't update. What worked was:
const page = this.usersPages[user.role - 1];
const oldUsers = page.data.filter(u => u.id != user.id);
Vue.set(page, 'data', [ ...oldUsers, response.data ]);
Works but doesn't look right... Can anyone help?
I had a similar problem before. I believe that this is a bug in vue. The reason for this is that Vue rendered the v-for already. Vue as of now does not know how to handle the changes in the array. What I did as a workaround for this is have a updateKey variable inside my script set to 0. Then increment this variable every time we update the array updateKey++. And we use this key for our component.
On child component,
<template>
<span
v-for="user in users"
:key="user.id"
>
<div :key="updateKey">
<span>{{ user.name }}</span>
<div>
<I
class="bi bi-pen-fill edit-icon m-pointer"
#click="onClickEdit(user)"
></i>
<I
class="bi bi-trash-fill delete-icon ms-2 m-pointer"
#click="onClickDelete(user)"
></i>
</div>
</div>
</span>
</template>
and onClickEdit(user) and onClickDelete(user) make sure you have updateKey++
The DOM won't update because you are not doing it reactively. To update this value reactively you should listen to some event (input for example), that you'll emit in the child component and pass the new(!) value to your property. But a better way is to bind your components via v-model.
P.S. - So you can see changes in the devtools because this is not a part of the Vue lifecycle, it`s just like a parser that watches actual data, but without context.
I decided to create an example to show #peperoneen how I did it, as I believed the way was correct. But in the example itself, it worked and in my project, it didn't, and the only difference was the way the list was initially filled.
To load the users I do a request to the API that returned the paged data, which I normally assigned with the '=' operator. However, I decided to do the assignment step by step and in the users part, I used the 'push' method. This way, the operations with the list using push and filter work normally, updating the DOM and without the need to use Vue.set
I think the problem was the way the list was being generated. I wasn't doing it reactively I guess...

How to improve large Vue component performance

I'm currently doing a form editor (like google forms). It's a vue component, basically looking like this:
<script>
export default {
data() {
return {
questions: [Array of objects describing form questions]
}
}
}
</script>
<template>
<div class="form-editor">
<!-- some interface not related to a specific question (like creating a new question) -->
<div v-for="question in questions" :key="question.id">
<input v-model=question.title>
<!-- more inputs binded to a specific question properties via v-model -->
</div>
</div>
</template>
It works fine, but when the number of questions gets bigger, it experiences huge performance issues. Should i extract logic related to a single question into a separate component to render it in a v-for loop? Or what do i do?

events and self referencing components vue.js

I have commenting system which allows 1 level thread. Meaning 1st level comment will look like
{
...content,
thread: []
}
where thread may have more comments in it. I though this is good for self-referencing component and List with Slots.
But after a while I do not know how to wire this thing up.
SingleComment component is given below
<template>
... *content*
<b-button
v-if="isCommentDeletable"
#click="handleDelete"
</b-button>
<div v-for="item in item.thread" :key="item._id">
<SingleComment class="ml-3"
:item="item"
/>
</div>
</template>
...
methods: {
handleDelete () {
this.$emit('remove')
},
}
...
components: {
'NewComment': NewComment, 'SingleComment': this
},
name: 'SingleComment'
}
</script>
List component classic list is recieving array of items as prop and is given by
<div v-for="item in items" ...
<slot
name="listitem"
:item="item"
/>
</div>
and this is the parent where I want to use these two components with modal
Parent
<AppModal
>
...
<List
class="my-1"
:items="comments.docs"
>
<template v-slot:listitem="{ item }">
<SingleComment
:item="item"
:remove="removeItem"
#remove="removeItem"
/>
</template>
</List>
I want to wire this thing up in Parent so I can use single modal for whole list.
Do I wire thins thing up with events? Or? Any sort of help is welcome. I am stuck. I can make some hacks but I really do not know how to deal with this self referencing components.
If you only have one level of nesting, you could simply pass the component itself as a slot, like so:
<Comment v-for="comment in comments" :key="comment.id" v-bind="comment">
<Comment v-for="thread in comment.thread" :key="thread.id" v-bind="thread" />
</Comment>
Then you will only have to worry about passing props one level deep, as if you only had a single list of comments. I created an example of this on CodeSandbox here: https://codesandbox.io/embed/vue-template-mq24e.
If you want to use a recursive approach, you'll just have to pass props and events around; there's no magic solution that steps around this. Update CodeSandbox example: https://codesandbox.io/embed/vue-template-doy66.
You could avoid explicitly passing the removeitem event listener down by having a removeitem action on your Vuex store that you map to your component.
My opinion here, is that simpler is better, and you don't need recursion for one level of nesting. Put yourself in the shoes of a future developer and make the code easy to read and reason about; that future developer may even be you when you haven't looked at the codebase in a few weeks.

Use a Vue component for multiple templates

I'm fairly new to Vue and I'm not even sure if I've phrased my question correctly. Here is what I am trying to achieve. I am using a card cascade from bootstrap, each card show part of a blog post. Each card has a link to a webpage for the whole blog.
To try and achieve this I have two vue files. cardCascade.vue and singleBlog.vue. My problem is at the moment I have to create a different singleBlog.vue files for each blog I have.
For example, suppose I have two blog posts in my database. cardCascade.vue will also have two links to individual blog posts. Blog post 1 will then use singleBlog1.vue and blog post 2 will then use singleBlog2.vue
What can I do so that I can achieve this more efficiently such that I only need one singleBlog.vue and it dynamically adjusts the content based on the link I select from cardCascade.vue?
What I have right now for parts of the cardCascade.vue
<b-card v-for="blog in blogs_duplicate" title="Title" img-src="https://placekitten.com/500/350" img-alt="Image" img-top>
<b-card-text>
<!--{{getBlogOfType("Vue",blog.id)}}-->
{{getBlogOfType("Vue",blog.id)}}
</b-card-text>
<b-card-text class="small text-muted">Last updated 3 mins ago</b-card-text>
</b-card>
Below is what I have right now for singleBlog.vue, keep in mind right now it just displays all the blog posts in a list.
<template>
<div id="single-blog">
<ul>
<article>
<li v-for="blog in blogs" v-bind:key="blog.id">
<h1>{{blog.title}}</h1>
<i class="fa fa-cogs" aria-hidden="true"></i>
<router-link v-bind:to="{name:'datascience-single', params: {blog_id: blog.blog_id}}">
<p>{{blog.content}}</p>
</router-link>
</li>
</article>
</ul>
</div>
</template>
<script>
import db from './firebaseInit'
export default{
data(){
return{
id:this.$route.params.id,
blogs:[],
}
},
created(){
//this.$http.get('http://jsonplaceholder.typicode.com/posts/' + this.id).then(function(data){
//this.blog = data.body;
db.collection('Blogs').orderBy('Type').get().then(querySnapshot =>{
querySnapshot.forEach(doc => {
//console.log(doc.data());
const data={
'id': doc.id,
'content': doc.data().Content,
'type': doc.data().Type,
'title': doc.data().Title,
'blog_id':doc.data().blog_id,
}
this.blogs.push(data)
})
})
}
}
</script>
It seems as though you should be giving your common component as a props information as to what it is rendering. Meaning you would make the call to the api in the parent and then make your child a "dumb" component. In your case you should make the calls to the api in cardCascade and then pass into your singleBlog component an id as props. Although in your case I do not see you using this component in the parent at all, where is it?
Like Michael said, the right way to do that is making a props and receives that prop from your router. Then, when you make your requisition, you'll pass this prop id. You can read more about props in here: https://v2.vuejs.org/v2/guide/components-props.html

Attach multiple v-models to the loop of dynamic child components in vuejs 2

Recently I ran into a problem, where I had to display many different components inside a loop, but: each of them should share it's state with parent (kinda knockout.js style). I was digging thru the docs where clearly was pointed out, that Vue.js pass properties one way down to childs, and those can eventually speak with some events. Also, docs says that there can be only one v-model per component, so finally I came up with something like this:
<li :is="field.type" v-for="(field, i) in fields" :key="i" :title="field.title" v-on:title-change="title = $event" :somevalue="field.somevalue" v-on:somevalue-change="somevalue = $event"></li>
And so on... Yet, after fifth parameter I quickly realized that the code is basically messy. Is there some less messy way to attach multiple two-way data bindings to child components?
The solution happened to be a .sync method and proper naming of events. While sync has been deprecated and removed in vue.js 2, since 2.3 version has been rewritten and added again in some similar form. As in fact it's only a syntatic sugar, my component now looks more decent I believe.
<ol>
<li :is="field.type"
v-for="(field, i) in fields"
:key="field.id"
v-bind.sync="field"
v-on:remove="fields.splice(i, 1)"></li>
</ol>
Vue.component('bool', {
template: '\
<li>\
<input type="text" v-bind:value="title" #input="$emit(\'update:title\', $event.target.value)">\
<button v-bind:value="value" #click="$emit(\'update:value\', !$event.target.value)" :class="{active: value}">{{value}}</button>\
<input type="checkbox" value="1" v-bind:checked="istrue" #change="$emit(\'update:istrue\', $event.target.checked)">\
<button #click="$emit(\'remove\')">Remove</button>\
</li>\
',
data () {
return {}
},
props: ['title', 'value', 'availablevalues']
})