skip to Main Content

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

2

Answers


  1. Chosen as BEST 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.


  2. 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 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>
    }
    </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);
            }
    

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search