I'm trying to make a simple Todo app to learn asp net core mvc.
I did the CRUD to manage the todos and it worked fine. For the next step i wanted to try adding Ajax to it (avoiding to reload the entire page), delete worked fine, create too, but when i want to edit one todo (which is basically a form) the response of the Ajax request sets all the inputs of all the todos at the same value.
If I update "Buy chocolat" by "Buy chocolate" as the title of one todo, all other todos will have a title "Buy chocolate".
If I refresh the page (or just the section containing todos) everything goes back to normal, which means the database updated just the todo I wanted to.
It's really weird and it probably comes from the fact that the inputs have the same name value (todo 1 title => todo.Title, todo 2 title => todo.Title, etc...) even though it works fine for all the rest.
Here's the page with the container of todos :
@model IEnumerable<TodoApp.Models.Todo>
@section Css{
<link href="/css/todos.css" rel="stylesheet" />
<link href="~/lib/fontawesome/css/all.css" rel="stylesheet" />
}
@{
ViewData["Title"] = "List of todos";
}
<h1>My list of Todos</h1>
<span style="color:red"></span>
<div id="main_container">
<i onclick="createTodo()" id="create-button" title="Add new todo"></i>
<div id="todos_container">
@await Html.PartialAsync("_TodoList", Model)
</div>
</div>
<partial name="_DeleteModal">
@section Scripts{
<script src="~/js/todos.js"></script>
}
Here's the foreach that displays all todos which also is the partial view "_TodoList" :
@model IEnumerable<TodoApp.Models.Todo>
@foreach (Todo todo in Model)
{
<form asp-action="Edit" asp-controller="Todos" data-id="@todo.Id">
<input type="hidden" asp-for="@todo.Id" id="[email protected]" />
<div >
<textarea autocomplete="off" placeholder="Put the title here..." asp-for="@todo.Title" id="[email protected]" ></textarea>
<textarea autocomplete="off" placeholder="Put the description here..." asp-for="@todo.Description" id="[email protected]" ></textarea>
</div>
<div >
<div >
<span></span>
<i alt="Saved"></i>
<span >Saved</span> @*Tooltip for edition*@
</div>
<div >
<div ><img src="~/assets/img/update.svg" alt="Updated at" /><span>@todo.UpdatedDate</span></div>
<a onclick="showDeleteModal(@todo.Id)" title="Delete todo">
<i ></i>
</a>
</div>
</div>
</form>
}
The beginning of the controller method :
[HttpPatch]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit([Bind("Id", "Title", "Description")] Todo todo)
{
if (ModelState.IsValid)
{
var matchingTodo = await _context.Todos.FindAsync(todo.Id);
if (matchingTodo != null)
{
if (GetConnectedUserId() == matchingTodo.UserId)
{
matchingTodo.Title = todo.Title;
matchingTodo.Description = todo.Description;
matchingTodo.UpdatedDate = DateTime.Now;
_context.Update(matchingTodo);
await _context.SaveChangesAsync();
var todos = GetTodosOfConnectedUser();
var partialView = PartialView("_TodoList", todos);
return partialView;
The GetTodosOfConnectedUser method (which return an Enumerable object of Todos that belongs to the user currently connected) :
private IEnumerable<Todo> GetTodosOfConnectedUser()
{
return _context.Todos.Where(t => t.UserId == Convert.ToInt32(HttpContext.User.FindFirst("user_id").Value)).OrderByDescending(t => t.UpdatedDate);
}
And the JS for the Ajax request :
${'.todo'}.on("change", function (ev) {
let form = ev.currentTarget;
editTodo(form);
});
function editTodo(form) {
try {
$.ajax({
type: 'PATCH',
url: form.action,
data: new FormData(form),
processData: false,
contentType: false,
success: function (res) {
$(".error-span").html("");
$('#todos_container').html(res);
},
error: function (err) {
console.log(err);
$(".error-span").html("An error occured please try again.");
}
});
return false;
}
catch (ex) {
console.log(ex);
}
}
Thank you for your time
CodePudding user response:
So, the problem is weird. Like, really weird.
I have followed what's happening step-by-step and everything is going smoothly and then... All the forms get the same inputs/textareas for no apparent reasons.
I believe it comes from the fact that I create one form for each todo, which is a really bad practice, probably never meant to be done in the first place. If you ever encounter this problem, just change the way you do it.
CodePudding user response:
I show my code and hope it can help you :
model
public class Todo
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string UpdatedDate { get; set; }
}
view
@model IEnumerable<TodoApp.Models.Todo>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<div id="todos_container">
@foreach (Todo todo in Model)
{
<form asp-action="Edit" asp-controller="Todos" data-id="@todo.Id">
<input type="hidden" asp-for="@todo.Id" id="[email protected]" />
<div >
<textarea autocomplete="off" placeholder="Put the title here..." asp-for="@todo.Title" id="[email protected]" ></textarea>
<textarea autocomplete="off" placeholder="Put the description here..." asp-for="@todo.Description" id="[email protected]" ></textarea>
</div>
<div >
<div >
<span></span>
<i alt="Saved"></i>
<span >Saved</span> @*Tooltip for edition*@
</div>
<div >
<div ><img src="~/assets/img/update.svg" alt="Updated at" /><span>@todo.UpdatedDate</span></div>
<a onclick="showDeleteModal(@todo.Id)" title="Delete todo">
<i ></i>
</a>
</div>
</div>
</form>
}
</div>
@*@section Scripts {*@
<script>
$('.todo').on("change", function (ev) {
let form = ev.currentTarget;
editTodo(form);
});
function editTodo(form) {
try {
$.ajax({
type: 'PATCH',
url: '/todoes/Edit',
data: new FormData(form),
processData: false,
contentType: false,
success: function (res) {
$(".error-span").html("");
$('#todos_container').html(res);
},
error: function (err) {
console.log(err);
$(".error-span").html("An error occured please try again.");
}
});
return false;
}
catch (ex) {
console.log(ex);
}
}
</script>
@*}*@
action
[HttpPatch]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit([Bind("Id", "Title", "Description")] Todo todo)
{
if (ModelState.IsValid)
{
var matchingTodo = await _context.Todos.FindAsync(todo.Id);
if (matchingTodo != null)
{
matchingTodo.Title = todo.Title;
matchingTodo.Description = todo.Description;
matchingTodo.UpdatedDate = DateTime.Now.ToString();
_context.Update(matchingTodo);
await _context.SaveChangesAsync();
var todos = GetTodosOfConnectedUser();
var partialView = PartialView("_TodoList", todos);
return partialView;
}
return BadRequest();
}
return View(todo);
}