Skip to content

When one form is updated (using Ajax) reloading the forms make them have the same values as the previously updated one

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 class="error-span" style="color:red"></span>

<div id="main_container">
    
    <i onclick="createTodo()" id="create-button" class="fas fa-plus-circle" 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 class="todo" asp-action="Edit" asp-controller="Todos" data-id="@todo.Id">
        <input type="hidden" asp-for="@todo.Id" id="[email protected]" />
        <div class="todo-up todo-row">
            <textarea autocomplete="off" placeholder="Put the title here..." class="todo-header" asp-for="@todo.Title" id="[email protected]" ></textarea>
            <textarea autocomplete="off" placeholder="Put the description here..." class="todo-description" asp-for="@todo.Description" id="[email protected]" ></textarea>
        </div>
        <div class="todo-down todo-row">
            <div class="todo-validation-row">
                <span></span>
                <i class="fa-regular fa-check todo-edit" alt="Saved"></i>
                <span class="tooltip-text">Saved</span> @*Tooltip for edition*@
            </div>
            <div class="todo-footer">
                <div class="todo-updated"><img src="~/assets/img/update.svg" alt="Updated at" /><span>@todo.UpdatedDate</span></div>
                <a onclick="showDeleteModal(@todo.Id)" title="Delete todo">
                    <i class="fas fa-trash"></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

Answer

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.