In-place record editing with AJAX has been a thorn in my side for a long time. I’ve got it figured out these days, but that wasn’t always the case. I wrote a post on this topic before I knew what I was doing and it’s time to set the record straight. @Html.Raw exposes many XSS-scripting security concerns and there was no need to use it for my intended purpose at that time. Anyway, let’s get to the fun stuff.
The Demo
It’s a little rough, but you get the gist of it. Unfortunately, dotNetFiddle doesn’t provide the greatest code-sharing experience so I’ll break it down a little bit.
The Controller and Model
public class HomeController : Controller { [HttpGet] public ActionResult Index() { return View(GetUsers()); } public List<UserViewModel> GetUsers() { //faux data from db var list = new List<UserViewModel>(); for (int i = 0; i < 10; i++) { list.Add(new UserViewModel() {Id = i, Username = "User_" + i, FirstName = "UserFirst " + i, LastName = "UserLast " + i}); } return list; } [HttpPost] public JsonResult Delete(int userId) { //perform deletion here /***************/ return Json("I deleted " + userId); } [HttpPost] public JsonResult Edit(UserViewModel user) { //save data here /***************/ //then return updated data as json return Json(user); } } public class UserViewModel { public int Id { get; set; } public string Username { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
The controller and model are the easiest parts of the whole thing. I left out the actual database logic but as you can see, there is the Index action which returns the main view, the Delete action which handles deleting the record, and the Edit action which handles the changes.
The View
Let’s look at the important parts of the view:
<div class="container"> <!-- Create the edit form in your controller --> <form id="edit"> <div class="row"> <input type="hidden" id="userId" name="Id"> <div class="col-md-2"> <label for="username">Username:</label> <input type="text" id="username" name="Username" class="form-control"> </div> <div class="col-md-2"> <label for="firstname">First Name:</label> <input type="text" id="firstname" name="FirstName" class="form-control"> </div> <div class="col-md-2"> <label for="lastname">Last Name:</label> <input type="text" id="lastname" name="LastName" class="form-control"> </div> </div> <input type="submit" class="btn btn-info" value="Save"> </form> <!-- Display your users as usual --> <table class="table"> @foreach(var item in @Model){ <tr> <td> @item.Username </td> <td> @item.FirstName </td> <td> @item.LastName </td> <td> <!-- Create dummy buttons to handle edit/delete --> <a href="#" class="btn btn-success edit-btn" data-id=@item.Id>Edit</a>| <a href="#" class="btn btn-danger delete-btn" data-id=@item.Id>Delete</a> </td> </tr> } </table> </div>
The trick to in-place record editing with AJAX is forgetting about the in-place part. What I mean is that at its roots, we are just moving around a simple edit form. We show it when we want to use it and hide it when we don’t.
What I’ve done in the html above is create a fairly basic form to represent the c# view model class and simply listed all the users that were passed from the controller. I’ve also added a dummy link for the edit and delete buttons that contain data-attribute references to the id of each respective user. In many cases, with a bit more work, this would be an acceptable user experience without the in-place stuff. But where’s the fun in that?
The JS/jQuery
You guessed it, the javascript is where the magic happens:
const deleteBtns = document.querySelectorAll(".delete-btn"); deleteBtns.forEach((btn)=>{ // Listen for clicks on your delete buttons, then send an ajax call back to your controller // for the Delete method, passing the id data-attribute to identify the record btn.addEventListener("click",function(){ $.ajax({ url: '@Url.RouteUrl(new{ action="Delete", controller="Home"})', data: {userId:parseInt(btn.dataset.id,10)}, type: 'POST', dataType:"json", complete: function(resp) { alert(resp.responseText); } }); //Hide (or delete) the row after a deletion is done to update the UI removeRow(btn); }) }) removeRow = (elemInRow)=>{ $(elemInRow).closest("tr").hide(); } const form = document.getElementById("edit"); // Hide the form initially $(form).hide(); const editBtns = document.querySelectorAll(".edit-btn"); editBtns.forEach((btn)=>{ // When an edit button is clicked, set the userId input field of the form // to point to the id of the record selected. // Then move the form into the same row. // Lastly, show the form. btn.addEventListener("click",function(){ document.getElementById("userId").value=btn.dataset.id; $(form).appendTo($(btn).closest("td")); $(form).show(); }) }) // Handle the form submit by passing it's input to the Edit function. // Update the UI to reflect the changes. // Hide the form again and send feedback via an alert. form.addEventListener("submit",function(event){ event.preventDefault(); $.ajax({ url: '@Url.RouteUrl(new{ action="Edit", controller="Home"})', data: $(this).serialize(), type: 'POST', dataType: 'json', complete: function(resp) { const jsonOb = JSON.parse(resp.responseText); updateRow(jsonOb.Username,jsonOb.FirstName,jsonOb.LastName,$(form).closest("tr")[0]); $(form).hide(); alert(`I saved: ${JSON.stringify(jsonOb)}`); } }); }) updateRow = (username, firstname, lastname,row)=>{ const tds = $("td",row); $(tds[0]).text(username); $(tds[1]).text(firstname); $(tds[2]).text(lastname); }
Breaking it Down
Hooking up the delete functionality is pretty straightforward:
const deleteBtns = document.querySelectorAll(".delete-btn"); deleteBtns.forEach((btn)=>{ // Listen for clicks on your delete buttons, then send an ajax call back to your controller // for the Delete method, passing the id data-attribute to identify the record btn.addEventListener("click",function(){ $.ajax({ url: '@Url.RouteUrl(new{ action="Delete", controller="Home"})', data: {userId:parseInt(btn.dataset.id,10)}, type: 'POST', dataType:"json", complete: function(resp) { alert(resp.responseText); } }); //Hide (or delete) the row after a deletion is done to update the UI removeRow(btn); }) }) removeRow = (elemInRow)=>{ $(elemInRow).closest("tr").hide(); }
We just need to loop through the buttons and watch for clicks. We will then fire off an ajax call with the appropriate user ID to the Delete function in the controller. Then we will use a little bit of jQuery to hide the row in the UI.
The next block of code handles part of the editing functionality:
const form = document.getElementById("edit"); // Hide the form initially $(form).hide(); const editBtns = document.querySelectorAll(".edit-btn"); editBtns.forEach((btn)=>{ // When an edit button is clicked, set the userId input field of the form // to point to the id of the record selected. // Then move the form into the same row. // Lastly, show the form. btn.addEventListener("click",function(){ document.getElementById("userId").value=btn.dataset.id; $(form).appendTo($(btn).closest("td")); $(form).show(); }) })
We want the hide the form initially and then set up each of the edit buttons to set the userId input element of the form to the correct user ID. Finally, we will move the form to the proper table cell and show it.
The last section is the form submit section:
// Handle the form submit by passing it's input to the Edit function. // Update the UI to reflect the changes. // Hide the form again and send feedback via an alert. form.addEventListener("submit",function(event){ event.preventDefault(); $.ajax({ url: '@Url.RouteUrl(new{ action="Edit", controller="Home"})', data: $(this).serialize(), type: 'POST', dataType: 'json', complete: function(resp) { const jsonOb = JSON.parse(resp.responseText); updateRow(jsonOb.Username,jsonOb.FirstName,jsonOb.LastName,$(form).closest("tr")[0]); $(form).hide(); alert(`I saved: ${JSON.stringify(jsonOb)}`); } }); }) updateRow = (username, firstname, lastname,row)=>{ const tds = $("td",row); $(tds[0]).text(username); $(tds[1]).text(firstname); $(tds[2]).text(lastname); }
We want to hi-jack the default submit behavior of the form and send it as an ajax request instead. Once we submit the changed data, then we need to update the UI.
In Conclusion…
There you have it, a quick and simple way to implement in-place ajax-based record editing. I wanted to post this because I’ve learned a lot since my original post. At the time, I was trying to store a lot of unnecessary data in html attributes and the use of @Html.Raw is known to be a huge security concern. This can be adapted to just about any front-end application with a few modifications.
Disclaimer: This project is meant to be a working demo and NOT secure code. You should use AntiForgeryTokens for all form submits and I would even recommend putting the delete-related code in it’s own form instead of a regular link (in order to add an AntiForgeryToken and conform to best-practices).