Vuejs - Duplicate keys detected using md-table iteration - vue.js

I am trying to show a material table for a database object that repeats its IDs.
<md-table v-model="data">
<md-table-toolbar>
<div class="md-toolbar-section-start">
<h1 class="md-title">MyTitle</h1>
</div>
<md-field md-clearable class="md-toolbar-section-end">
<md-input placeholder="Search..." v-model="search" #input="searchOnTable" />
</md-field>
</md-table-toolbar>
<md-table-empty-state
:md-label="grid.length < 1 ? 'Nothing here yet' : `No users with the term '${search}' found`">
</md-table-empty-state>
<md-table-row slot="md-table-row" slot-scope="{ item }" v-bind:key="`row-${item.id}-${data.indexOf(item)}`" :class="getClass(item)" md-selectable="single">
<md-table-cell md-label="ID" v-if="user.role === 1" :key="data.indexOf(item)">{{item.id}}</md-table-cell>
<md-table-cell md-label="Name">{{item.name}}</md-table-cell>
<md-table-cell md-label="Type">{{item.type}}</md-table-cell>
<md-table-cell md-label="User" v-if="user.role === 1">
<span>
<span v-if="user.role === 1 && !item.nameUser">
-
</span>
<span v-if="item.nameUser">
{{item.nameUser}}
</span>
</span>
</md-table-cell>
<md-table-cell md-label="Status">
<span>
{{getStatus(item.status)}}
</span>
</md-table-cell>
<md-table-cell md-label="">
<span>
<md-button #click="start(item.id)" v-if="!item.status">
HandleEval
</md-button>
<md-button :to="`/final/${item.id}`" v-else-if="(item.status === 2 || item.status === 3) && user.role === 1">
FinishEval
</md-button>
<md-button :to="(eval_fase == 8) ? `/fase2/${item.id}/${item.user_evaluator_id}` : (eval_fase == 4) ? `/fin/${item.id}` : `/ev/${item.id}`" v-else-if="item.status === 8 || item.status === 1">
Continuar avaliação
</md-button>
<md-button #click="remove(item.id)" v-if="!item.status || ((item.status === 2 || item.status === 3) && user.role === 1) || (item.status === 8 || item.status === 1)">
<md-icon>delete</md-icon>
</md-button>
</span>
</md-table-cell>
</md-table-row>
</md-table>
The problem is when i access that page, i get this error:
vue.runtime.esm.js?2b0e:619 [Vue warn]: Duplicate keys detected: '62'. This may cause an update error.
found in
---> at src/components/MdTable/MdTable.vue
at src/components/Grid.vue
at src/components/MdContent/MdContent.vue
at src/components/MdApp/MdAppContent.vue
at src/components/MdApp/MdAppSideDrawer.vue
at src/views/Grid.vue
at src/App.vue
data looks like that:
[
{"date":"Thu, 10 Jun 2021 19:04:14 GMT","some_id":106,"id":210,"name":null,"nameUser":"Test","stage":null,"status":1,"status_form":0,"type":"compreensive","user_id2":4,"user_id":212},
{"date":"Thu, 10 Jun 2021 19:04:14 GMT","some_id":107,"id":210,"name":null,"nameUser":"Option","stage":null,"status":1,"status_form":0,"type":"compreensive","user_id2":13,"user_id":212},
{"date":"Thu, 10 Jun 2021 19:04:14 GMT","some_id":108,"id":210,"name":null,"nameUser":"Test2","stage":null,"status":1,"status_form":0,"type":"compreensive","user_id2":20,"user_id":212},
{"date":"Thu, 10 Jun 2021 19:04:14 GMT","some_id":109,"id":210,"name":null,"nameUser":"Tester","stage":null,"status":1,"status_form":0,"type":"compreensive","user_id2":61,"user_id":212},
]

Based on this line and this one in source code, Try out md-model-id="some_id"
<md-table v-model="data" md-model-id="some_id">
since this prop takes id by default value which also used as key, in your case id has the same value.

Related

Creating pagination component

I am trying to create pagination component. For e.g if my API returns "pages": 9
For example, if I have 9 pages, I want to cut the list at 5 and add three dots like on the image. I want to be able to provide at which index I can cut the list. Whats the best way to do this? I am approaching this wrong?
<div v-for="index in pages" class="flex">
<button>{{index}}</button>
</div>
Assuming you receive cutIndex and apiPages as props to your component, then your template could look something like the following:
<div v-for="page in Math.min(cutIndex, apiPages)" class="flex">
<button>{{page}}</button>
</div>
<template v-if="cutIndex < apiPages">
<div class="flex">
<button>...</button>
</div>
<div class="flex">
<button>{{apiPages}}</button>
</div>
</template>
this code works for me, and here is the key part
<template>
...
<template v-for="index in pages"
v-if="(index < 4 || (length - index) < 3) || Math.abs(index - value) < 2">
<span v-if="index === (length - 2) && value < (length - 4)">
...
</span>
<button #click.prevent="updatePage(index)">
{{ index }}
</button>
<span v-if="(index === 3 && value > 5)">
...
</span>
</template>
...
</template>
the result:
<template>
<nav class="flex items-center justify-center" role="pagination">
<!-- go to previous page -->
<a :key="`${$route.name}-arrow-${value > 1 ? value - 1 : 1}`"
:href="getFullPath(value > 1 ? value - 1 : 1)"
:title="$t('previous_page')"
#click.prevent="updatePage(value > 1 ? value - 1 : 1)"
:disabled="value === 1"
class="arrow pop-btn default rounded-sm"
v-waves>
<i class="mdi-chevron-left mdi"></i>
</a>
<template v-for="index in length"
v-if="(index < 4 || (length - index) < 3) || Math.abs(index - value) < 2">
<span v-if="index === (length - 2) && value < (length - 4)">
...
</span>
<a :href="getFullPath(index)"
:title="$t('page_index', {index: index})"
#click.prevent="updatePage(index)"
class="pop-btn number default rounded-sm"
:class="{
'active': index === value
}"
v-waves>
{{ index }}
</a>
<span v-if="(index === 3 && value > 5)">
...
</span>
</template>
<!-- go to next page -->
<a :href="getFullPath(value === length ? value : value + 1)"
:title="$t('next_page')"
#click.prevent="updatePage(value === length ? value : value + 1)"
:disabled="value === length"
class="arrow pop-btn default rounded-sm"
v-waves>
<i class="mdi-chevron-right mdi"></i>
</a>
</nav>
</template>
<script type="text/javascript">
export default{
emits: ['update:value],
props: {
length: {
required: true,
type: Number
},
// the page filter
value: {
required: true,
type: Number
}
},
methods: {
updatePage(index){
this.$emit('update:value', index);
},
getFullPath(page){
let query = {...this.$route.query};
page === 1 ? delete query.page : query.page = page;
return this.$router.resolve(
this.r({
query: query,
name: this.$route.name,
params: this.$route.params
})
).href;
}
}
}
</script>
by the way, <a> may better than <button> for SEO

Cannot read property 'focus' of undefined in VUE When setting focus to button

I am new to vue I have component which if the endpoint fails, calls my generic 'Error' modal. All this is working fine but I keep getting the following error:
Cannot read property 'focus' of undefined
This only happens for the else part of my method function.
For this specific issue is I my 'failedPrcess' equals any of the following, this is when I get is, all others are fine:
existOrderSearchProdOrders
stockSearchStockLevels
cartFetchCouriers
Code
<template>
<div class="modal fade danger-modal" id="errorModal" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle" aria-hidden="true"
data-keyboard="false" data-backdrop="static" style="z-index: 99999">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content danger-modal-content">
<div class="modal-header danger-modal-headerfooter">An error has occurred</div>
<div class="modal-body">
<p v-if="failedProcess === 'appGetAccount' || failedProcess === 'existOrderSearchProdOrders' || failedProcess === 'stockSearchStockLevels'
|| failedProcess === 'cartFetchCouriers'">
{{ contactTxt | capitalize }}
</p>
<p v-else-if="errorCount < 3">If the error continues, {{ contactTxt }}</p>
<p v-else>As the error has continued, {{ contactTxt }}</p>
<p>
<b>
01234 567890
<br />
Open from 00:00 to 07:00
</b>
</p>
<p>Advising of what you were doing when the error occurred.</p>
</div>
<div class="modal-footer danger-modal-headerfooter">
<a v-if="failedProcess === 'appGetAccount'" ref="logoutButton" class="btn btn-primary" :class="logoutButtClicked" #click="logoutClicked = true" href="/site/logout">
<span v-if="!logoutClicked" id="logoutButtonLabel">Logout</span>
<span v-else id="logoutSpinner">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Logging out
</span>
</a>
<router-link v-else-if="failedProcess === 'fetchOrderReportDetails'" to="/review" tag="button"
ref="existOrdersButton" class="btn btn-primary" type="button" data-dismiss="modal" #click.native="closeButton">
Return to existing orders
</router-link>
<button v-else-if="errorCount < 3 && (failedProcess !== 'productsFetchProducts' && failedProcess !== 'existOrderSearchProdOrders'
&& failedProcess !== 'stockSearchStockLevels' && failedProcess !== 'cartFetchCouriers')" ref="closeButton" class="btn btn-primary"
type="button" data-dismiss="modal" #click="closeButton">
Close
</button>
<router-link v-else to="/" tag="button" ref="homeButton" class="btn btn-primary" type="button" data-dismiss="modal" #click="closeButton">
Return to homepage
</router-link>
</div>
</div>
</div>
</div>
</template>
<script>
import * as params from '../params';
export default {
name: "ErrorModal",
data() {
return {
contactTxt: 'please contact us on:',
errorCount: 0,
failedProcess: '',
}
},
mounted() {
VueEvent.$on('show-error-modal', (failedProcess) => {
if (this.failedProcess !== failedProcess) {
this.errorCount = 0;
}
this.failedProcess = failedProcess;
$('#errorModal').modal('show').on('shown.bs.modal', this.focus);
});
},
methods: {
focus() {
if (this.failedProcess === 'appGetAccount') {
this.$refs.logoutButton.focus();
} else if (this.failedProcess === 'fetchOrderReportDetails') {
this.$refs.existOrdersButton.$el.focus();
} else if (this.errorCount < 3 && this.failedProcess !== 'productsFetchProducts') {
this.$refs.closeButton.focus();
} else {
this.$refs.homeButton.$el.focus();
}
},
}
}
</script>`enter code here`
I've tried using v-if before and I also had similar problems and the best solution I found was, instead of using v-if/v-else-if/v-else, use v-show instead to perform conditional rendering.
Also, as the Vue.js doc says:
Generally speaking, v-if has higher toggle costs while v-show has higher initial render costs. So prefer v-show if you need to toggle something very often, and prefer v-if if the condition is unlikely to change at runtime.

How to set class to child element inside v-for loop

I need some help, I have v-for loop which outputs elements of array referenceDetailsDocumentsData, I need to check at the same time if the id of this element exists in another array documentsData, in this case I need to add custom class to child of this element.
<div class="loading-doc-item"
v-for="referenceDetails in referenceDetailsDocumentsData"
:key="referenceDetails.id">
<div class="loading-doc-show">
{{ referenceDetails.name }}
<span class="upload-status" v-if="checkUploadedDocuments(referenceDetails.id)">
<i class="fa fa-check-circle"></i>
</span>
<span class="upload-status" v-else>
<i class="fa fa-check"></i>
</span>
</div>
</div>
methods() {
checkUploadedDocuments(id) {
return this.documentsData.filter(item => item.id === id);
}
}
In my case, I am getting an error
Error in render: "TypeError: this.documentsData.filter is not a
function"
Your logic is wrong - the method checkUploadedDocuments will return Array but it must return Boolean.
<div class="loading-doc-item"
v-for="referenceDetails in referenceDetailsDocumentsData" :key="referenceDetails.id">
<div class="loading-doc-show">
{{ referenceDetails.name }}
<span class="upload-status">
<i class="fa"
:class="{documentsData && documentsData.length &&
documentsData.findIndex(item => item.id === referenceDetails.id) !== -1
? 'fa-check-circle' : 'fa-check'}"></i>
</span>
</div>
</div>

Slow DOM Change with v-show

I've been troubleshooting an issue where, inside of a nested v-for loop, a simple v-show directive is taking up toward 4 seconds to actually apply the display: none; attribute in the DOM.
The basic layout is this. I've changed the actual naming to be all about cars; because why not!
I do apologize for the wall of code, but I felt it was relevant to include a basic jist of the component structure.
<div class="vehicle-history" v-if="fetchComplete && !fetchError">
<h3 class="header-bold">History</h3>
<div class="history-container" v-for="car in vehicles.cars" :key="car.id">
<div class="vehicle-header">
<div class="toyota" v-if="car.model == 'toyota'">
<span class="model-id">#{{ car.type }}_#{{ car.id }}</span> - #{{ car.title }}
</div>
<div class="audi" v-if="car.model == 'audi'">
<span class="model-id">#{{ car.type }}_#{{ car.id }}</span> - #{{ car.title }}
</div>
<div class="benz" v-if="car.model == 'benz'">
<span class="model-id">#{{ car.type }}_#{{ car.id }}</span> - #{{ car.title }}
</div>
<div class="rolls" v-if="car.model == 'rolls'">
<span class="model-id">#{{ car.type }}_#{{ car.id }}</span> - #{{ car.title }}
</div><br />
<small v-for="color in car.colors">#{{ color.name }} </small>
</div>
<div class="vehicle-body">
<div class="description">#{{ car.description }}</div>
<div class="review-container" v-if="car.reviews.length > 5">
<div class="review" v-for="n in (0, 4)" :key="car.reviews[n].id">
<strong>#{{ car.reviews[n].rating }}</strong> - #{{ car.reviews[n].content }}<br />
<small>#{{ car.reviews[n].created_at.date }} #{{ car.reviews[n].created_at.timezone }}</small>
<span v-show="n == 4 && !car.displayFull"><br />
<button type="button"
class="btn btn-link"
data-toggle="collapse"
v-on:click="car.displayFull = true"
v-bind:data-target="'#expanded-reviews-' + car.id">More details
</button>
</span>
</div>
<div class="collapse" v-bind:id="'expanded-reviews-' + car.id">
<div class="review" v-for="n in (4, car.reviews.length-1)" :key="car.reviews[n].id">
<strong>#{{ car.reviews[n].rating }}</strong> - #{{ car.reviews[n].content }}<br />
<small>#{{ car.reviews[n].created_at.date }} #{{ car.reviews[n].created_at.timezone }}</small>
<span v-if="n == car.reviews.length-1"><br />
<button type="button"
class="btn btn-link"
data-toggle="collapse"
v-on:click="car.displayFull = false"
v-bind:data-target="'#expanded-reviews-' + car.id">Less details
</button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
What I've tried
Adding the :key to the relevant loops
Moving the v-show attribute from the button (where it was originally at) to the parent span.
Wrapping it in <keep-alive> (which didn't work at all, it broke visibility as a whole)
Swapping v-show for v-if and vice-versa
The expected output
What I'm trying to accomplish is to make sure the button for "More details" hides away when the accordion gets expanded (that is, when the "More details" button is initially clicked) and re-appears if the "Less details" button is clicked.
The actual output
Whilst it does the above, there's a delay of between 2-5 seconds from clicking the button, to it getting the display: none; attribute applied in the DOM. The change to the Vue variable however appears instant.
Do you have any clue why this may be, and if there's a good workaround for this? Thank you, as always!
UPDATE
Revised code example below based on a number of suggestions. This is a flat out copy of existing code, so no changes to naming this time around.
<div class="incident-history text-center col-xs-12 padding-bottom" v-if="fetchComplete && !fetchError">
<h3 class="heading3 black-text">Incident History</h3>
<div class="incident-container" v-for="incident in responseData.incident.resolved" :key="incident.id">
<div class="incident-header">
<div :class="'title-' + incident.classification">
<span class="incident-id">#{{ incident.type }}_#{{ incident.id }}</span> - #{{ incident.title }}
</div><br />
<small v-for="monitor in incident.monitors">#{{ monitor.name }} </small>
</div>
<div class="incident-body">
<div class="description">#{{ incident.content }}</div>
<div class="update" v-for="(comment, index) in incident.comments.slice(0,5)">
<strong>#{{ comment.type }}</strong> - #{{ comment.content }}<br />
<small>#{{ comment.created_at.date }} #{{ comment.created_at.timezone }}</small>
</div>
<div class="collapse"
v-bind:id="'expanded-update-' + incident.id"
v-if="incident.comments.length > 4">
<div class="update" v-for="(comment, index) in incident.comments.slice(5)">
<strong>#{{ comment.type }}</strong> - #{{ comment.content }}<br />
<small>#{{ comment.created_at.date }} #{{ comment.created_at.timezone }}</small>
</div>
</div>
<button type="button"
class="btn btn-link"
data-toggle="collapse"
v-show="incident.comments.length > 4 && !incident.displayFull"
v-on:click="incident.displayFull = !incident.displayFull"
v-bind:data-target="'#expanded-update-' + incident.id">Show more
</button>
<button type="button"
class="btn btn-link"
data-toggle="collapse"
v-show="incident.comments.length > 4 && incident.displayFull"
v-on:click="incident.displayFull = !incident.displayFull"
v-bind:data-target="'#expanded-update-' + incident.id">Show less
</button>
</div>
</div>
</div>
I will note that the above code block is run inside of yet another for-loop. The basic topology is this (lengths may vary)
responseData: Object
incident: Object
active: Array[4]
0: Object
1: Object
Property: SomeValue
Property: SomeValue
Property: SomeValue
Property: SomeValue
Property: SomeValue
Comments: Array[3]
0: Object
1: Object
Property: SomeValue
Property: SomeValue
2: Object
2: Object
3: Object
Alternative Solution
I've come up with a working, and likely better alternative for the changing of "Read more" to "Read less" and vice versa.
button.full-details[aria-expanded="true"]:after {
content: 'Show less';
}
button.full-details[aria-expanded="false"]:after {
content: 'Show more';
}
This works fine, and just needs adding a few lines of CSS as well as a class to the button (or targeting the Bootstrap button class, but that's not great for many reasons).
That said, I'm going to leave this one open for a little bit to hopefully gain an understanding into why the issue arose with Vue in the first place.
It is surprising to hear 2-5 secs lag in display, but then there's quite a bit going on in that template. Hard to tell if suggested changes will impact the page visuals, without a test system.
The button(s) seems most problematic, creates one per review but only needs one per car, has to evaluate v-show or v-if for all of them.
Have one button outside the reviews loop,
<button class="btn btn-link" type="button"
data-toggle="collapse" :data-target="'#expanded-reviews-' + car.id"
#click="car.displayFull = !car.displayFull">
{{ buttonPrompt(car.displayFull) }}
</button>
I'd also consider creating a car child component, even a review grandchild component, as reasoning about the layout becomes difficult when it's that busy.
The final thing is so obvious, I'm thinking it must be an artifact of the domain renaming.
This
<div class="toyota" v-if="car.model == 'toyota'">
<span class="model-id">#{{ car.type }}_#{{ car.id }}</span> - #{{ car.title }}
</div>
<div class="audi" v-if="car.model == 'audi'">
<span class="model-id">#{{ car.type }}_#{{ car.id }}</span> - #{{ car.title }}
</div>
<div class="benz" v-if="car.model == 'benz'">
<span class="model-id">#{{ car.type }}_#{{ car.id }}</span> - #{{ car.title }}
</div>
<div class="rolls" v-if="car.model == 'rolls'">
<span class="model-id">#{{ car.type }}_#{{ car.id }}</span> - #{{ car.title }}
</div>
could become this
<div :class="car.model">
<span class="model-id">{{ car.type }}_{{ car.id }}</span> - {{ car.title }}
</div>
Also, what is this? #{{?
Edit after comment - on the question of how many buttons are created
It sounds like you are expecting only one or two buttons to be rendered? Here's a test.
From a spec for Cars.vue created with your template,
Test data has 6 reviews,
<script>
export default {
data () {
return {
fetchComplete: true,
fetchError: false,
cars: [
{
model: 'toyota',
type: 'hatch',
id: 1,
title: 'corolla',
description: 'grey corolla',
colors: [{name: 'grey'}, {name: 'grey'}, {name: 'grey'}],
reviews: [
{rating: 2, content: 'its grey', created_at: {date: ''}},
{rating: 2, content: 'its grey', created_at: {date: ''}},
{rating: 2, content: 'its grey', created_at: {date: ''}},
{rating: 2, content: 'its grey', created_at: {date: ''}},
{rating: 2, content: 'its grey', created_at: {date: ''}},
{rating: 2, content: 'its grey', created_at: {date: ''}},
]
}
]
}
},
The test,
it('count the buttons', () => {
const buttons = wrapper.element.querySelectorAll('button');
console.log(buttons)
})
Results,
LOG LOG: Object{
0: <button type="button" data-toggle="collapse" data-target="#expanded-reviews-1" class="btn btn-link">More details</button>,
1: <button type="button" data-toggle="collapse" data-target="#expanded-reviews-1" class="btn btn-link">More details</button>,
2: <button type="button" data-toggle="collapse" data-target="#expanded-reviews-1" class="btn btn-link">More details</button>,
3: <button type="button" data-toggle="collapse" data-target="#expanded-reviews-1" class="btn btn-link">More details</button>,
4: <button type="button" data-toggle="collapse" data-target="#expanded-reviews-1" class="btn btn-link">Less details</button>,
length: 5}
Then, there's the question of whether the change detection cycle is smart enough not to re-evaluate button creation each cycle. I'll see if we can test that too.
Placement of v-if
It just occurred to me, this
<button class="btn btn-link" ... >
<span v-if="n === car.reviews.length-1">Less details</span>
</button>
might be better as this
<button class="btn btn-link" ... v-if="n === car.reviews.length-1">
Less details
</button>
Missing first review
The v-for="n in (0, 4)" loop seems to miss out the first review. If I set the ratings to be sequential numbers, the first is not displayed.
<div class="review" v-for="n in (0, 4)">
<strong>#{{ car.reviews[n].rating }}</strong> - #{{ car.reviews[n].content }}
Try
<div class="review" v-for="(review,n) of car.reviews.slice(0,5)" :key="review.id">
<strong>#{{ review.rating }}</strong> - #{{ review.content }}
...
<div class="review" v-for="(review,n) of car.reviews.slice(5)">
(keeping n for button v-if expression).

Vue - check if you are on the last prop of a v-for loop

If I have the following data property:
person: {name: 'Joe', age: 35, department: 'IT'}
And wanted to loop through and output it as follows:
name: Joe, age: 35, department: IT
So far I have:
<span v-for="(val, key) in person">{{key}}: {{val}}, </span>
But this displays:
name: Joe, age: 35, department: IT,
with an extra comma on the end, how can I have it detect that it's the last prop and not show the comma? I thoughta v-show or v-if may be the solution but can't quite figure out how to make it work.
Here is one way.
<span v-for="(val,key,index) of person">
key: {{key}}, val: {{val}}, index: {{index}}
<span v-if="index != Object.keys(person).length - 1">, </span>
</span>
Here's a solution if you're looping through an array, rather than an object:
<div id="app">
<div v-for="(item, index) in items">
<div v-if="index == items.length - 1">yes</div>
{{ item }}, {{ index }}
</div>
</div>
You can also "cheat" by inserting the comma before each item, as it's easier to check for the first item (index !== 0).
<span v-for="(val, key, index) in person">
<span v-if="index !== 0">, </span>
{{key}}: {{val}}
</span>
You can do that with a computed to see if the current index (third parameter forv-if) is the last property:
computed: {
last(){
return Object.keys(this.person).length-1;
}
}
Then in your v-for:
<span v-for="(val, key, index) in person">{{key}}: {{val}}<span v-if="index !== last">, </span> </span>
Here's the JSFiddle: https://jsfiddle.net/wv2ujxvn/
This also works:
<span v-for="(value,key) in persons" :key='key'>
{{key}}: {{val}}
<span v-if="key+1 != persons.length">, </span>
</span>
A pity there is no shortcut provided by Vue.
I personally prefer using a small CSS:
<div class="list">
<span>Item 1</span>
<span>Item 2</span>
<span>Item 3</span>
</div>
.list span:not(:last-child)::after {
content: ',';
}
If you want to store the knowledge about this pattern in code instead of on Stack Overflow, you could create a component like this:
<template>
<span v-if="show"><slot></slot></span>
</template>
<script>
export default {
name: 'separator',
props: ['items', 'index'],
computed: {
show () {
return this.index !== (Array.isArray(this.items) ? this.items : Object.keys(this.items)).length - 1
}
}
}
</script>
This doesn't necessarily make the code shorted, but easier to remember:
<span v-for="(val, key, index) of person">key: {{key}}, val: {{val}}
<separator :items="person" :index="index">, </separator>
</span>