Как исправить ошибку vue.js:634 [Vue warn]: Duplicate keys detected: '80'. This may cause an update error
Пытаюсь реализовать фронт на JS для REST приложения на Spring
Приложение - список дел. Добавление дел проходит нормально. Например, я добавил 2 дела:

При попытке отредактировать дело в форму передаются значения полей с именем, описанием, автором. Допустим, мы что-то поменяли, нажали кнопку "Сохранить", то мы видим 2 разных дела, но у них, почему-то, одинаковый ID!

Лог ошибки в консоли браузера:
vue.js:634 [Vue warn]: Duplicate keys detected: '82'. This may cause an update error.
found in
---> <TaskList>
<Root>
warn @ vue.js:634
checkDuplicateKeys @ vue.js:6241
updateChildren @ vue.js:6179
patchVnode @ vue.js:6314
updateChildren @ vue.js:6188
patchVnode @ vue.js:6314
updateChildren @ vue.js:6188
patchVnode @ vue.js:6314
patch @ vue.js:6475
Vue._update @ vue.js:3949
updateComponent @ vue.js:4067
get @ vue.js:4478
run @ vue.js:4553
flushSchedulerQueue @ vue.js:4311
(anonymous) @ vue.js:1989
flushCallbacks @ vue.js:1915
Promise.then (async)
timerFunc @ vue.js:1942
nextTick @ vue.js:1999
queueWatcher @ vue.js:4403
update @ vue.js:4543
notify @ vue.js:745
mutator @ vue.js:897
(anonymous) @ main.js:58
Promise.then (async)
e.then @ [email protected]:7
(anonymous) @ main.js:56
Promise.then (async)
e.then @ [email protected]:7
save @ main.js:55
invokeWithErrorHandling @ vue.js:1863
invoker @ vue.js:2188
original._wrapper @ vue.js:7547
Код Контроллера ниже:
package ru.skillbox.ifomkin.todolist.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.skillbox.ifomkin.todolist.entity.Task;
import ru.skillbox.ifomkin.todolist.service.TaskService;
import java.util.Date;
import java.util.List;
@RestController
@RequestMapping("task")
public class RestConroller {
@Autowired
private TaskService service;
@GetMapping
public List<Task> getAll() {
return service.getAll();
}
@GetMapping("{id}")
public Task taskById(@PathVariable int id) {
return service.findById(id);
}
@PostMapping
public Task add(@RequestBody Task newTask) {
newTask.setCreationDate(new Date());
service.save(newTask);
return newTask;
}
@PutMapping("{id}")
public Task update(@RequestBody Task task, @PathVariable int id) {
return service.updateTask(task, id);
}
@DeleteMapping("{id}")
public void delete(@PathVariable int id) {
service.deleteById(id);
}
}
Код main.js:
function getIndex(list, id) {
for (var i = 0; i < list.length; i++) {
if (list[i].id === id) {
return i;
}
}
return -1;
}
var taskApi = Vue.resource('/task{/id}');
Vue.component('task-form', { // Форма задания
props: ['tasks', 'taskAttr'],
data: function () {
return {
id: '',
name: '',
description: '',
author: ''
}
},
watch: {
taskAttr: function (newVal, oldVal) {
this.id = newVal.id;
this.name = newVal.name;
this.description = newVal.description;
this.author = newVal.author;
}
},
template:
'<form><div class="form-row">' +
'<div class="col">' +
'<input type="text" class="form-control" placeholder="Имя" v-model="name" />' +
'</div>' +
'<div class="col">' +
'<input type="text" class="form-control" placeholder="Описание" v-model="description" />' +
'</div>' +
'<div class="col">' +
'<input type="text" class="form-control" placeholder="Автор" v-model="author" />' +
'</div>' +
'<div class="col">' +
'<input type="button" class="btn btn-success" value="Сохранить" v-on:click="save" />' +
'</div>' +
'</div></form>',
methods: {
save: function () {
var task = {
name: this.name,
description: this.description,
author: this.author
};
if (this.id) {
taskApi.update({id: this.id}, task).then(result =>
result.json().then(data => {
var index = getIndex(this.tasks, data.id);
this.tasks.splice(index, 1, data);
this.name = ''; //Обнуляем поля
this.description = '';
this.author = '';
})
)
} else {
taskApi.save({}, task).then(result =>
result.json().then(data => {
this.tasks.push(data);
this.name = ''; //Обнуляем поля
this.description = '';
this.author = '';
})
)
}
}
}
});
Vue.component('task-row', { //Строчка с заданием
props: ['task', 'editTask', 'tasks'],
template:
'<tr>' +
'<th scope="row>">{{task.id}}</th>' +
'<td>{{task.name}}</td>' +
'<td>{{task.description}}</td>' +
'<td>{{task.author}}</td>' +
'<td>{{task.creationDate}}</td>' +
'<td><input type="button" class="btn btn-info" value="Изменить" v-on:click="edit"/></td>' +
'<td><input type="button" class="btn btn-danger" value="Удалить" v-on:click="del"/></td>' +
'</tr>',
methods: {
edit: function () {
this.editTask(this.task);
},
del: function () {
taskApi.remove({id: this.task.id}).then(result => {
if (result.ok) {
this.tasks.splice(this.tasks.indexOf(this.task), 1)
}
})
}
}
});
Vue.component('task-table-header', { //Временная шапка таблицы
template: '' +
'' +
' <thead class="thead-dark">\n' +
'<br>' +
' <tr>' +
' <th scope="col">ID</th>' +
' <th scope="col">Имя</th>' +
' <th scope="col">Описание</th>' +
' <th scope="col">Автор</th>' +
' <th scope="col">Дата создания</th>' +
' <th scope="col" colspan="2">Редактирование</th>' +
' </tr>' +
' </thead>'
});
Vue.component('task-list', { //Список заданий с циклом
props: ['tasks'],
data: function () {
return {
task: null
}
},
template: '' +
'<div>' +
'<task-form :tasks="tasks" :taskAttr="task"/>' +
'<table class="table">' +
'<task-table-header />' +
'<tbody>' +
'<task-row v-for="task in tasks" v-bind:key="task.id" :tasks="tasks" :task="task" :editTask="editTask"/>' +
'</tbody>' +
'</table>' +
'</div>',
created: function () {
taskApi.get().then(result =>
result.json().then(data =>
data.forEach(task => this.tasks.push(task))
))
},
methods: {
editTask: function (task) {
this.task = task;
}
}
});
var app = new Vue({
el: '#app',
template: '<task-list :tasks="tasks"/>',
data: {
tasks: []
}
})
Код TaskService:
package ru.skillbox.ifomkin.todolist.service;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import ru.skillbox.ifomkin.todolist.entity.Task;
import ru.skillbox.ifomkin.todolist.repository.TaskRepository;
import java.util.ArrayList;
import java.util.List;
@org.springframework.stereotype.Service
public class TaskService {
@Autowired
private TaskRepository repository;
public List<Task> getAll() {
Iterable<Task> taskIterable = repository.findAll();
List<Task> tasks = new ArrayList<>();
for (Task task : taskIterable) {
tasks.add(task);
}
return tasks;
}
public Task updateTask(Task task, int id) {
Task currentTask = repository.findById(id);
currentTask.setAuthor(task.getAuthor());
currentTask.setDescription(task.getDescription());
currentTask.setName(task.getName());
repository.save(currentTask);
return currentTask;
}
public Task findById(int id) {
return repository.findById(id);
}
public void save(Task task) {
repository.save(task);
}
public void deleteById(int id) {
repository.deleteById(id);
}
}
Код index.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ToDoList</title>
<!-- Vue JS -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- Vue resource -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
<div class="container">
<a th:href="@{/}" class="navbar-brand" th:text="ToDoList"/>
<p th:text="'A simple time management application'"/>
<div id="app"></div>
<div>
<a href="thymeleaf">Go to Thymeleaf page</a>
</div>
</div>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
Что надо поправить, чтобы этого бага не было?
Ответы (1 шт):
Автор решения: I am from Mordor
→ Ссылка
Разобрался, проблема была с вычислением индекса
Функция поиска должна выглядеть так:
function getIndex(tasks, id) {
let i = 0;
let index = 0;
tasks.forEach(function (element) {
if (element.id === id) {
index = i;
}
i++;
})
return index;
}
Ответ нашёл Вот тут