TransFORMing your Forms
Seth A Burleson
Posted on June 21, 2021
On my new Bugtracker project (Try it out here!) I was putting together a button that the user can use to change the state of the project, but I needed this button, and other page elements, to change based on that state.
The big frustration was that when I clicked the button, it was refreshing the page, meaning if a user had a new ticket, finished development and testing, they had reloaded the page a few times without seriously changing the data. I wanted to change the data without refreshing the page, which was where I learned about AJAX, and my life changed completely....
A few things to realize before we start, I'm using the AdminLTE template for most of my formatting on this project. The project is written on the C# stack, but it will work fine for other tech such as Node, flask, or any framework that uses MVC. Most of what we're discussing here is frontend.
Keeping it fresh, without refresh
Before I implemented ajax, I had a controller action which updated the status when the form (which only contains a button and hidden input) submitted, it then returned the new Ticket Details page. At first, I used this action with ajax to perform the same action without refreshing.
First, lets look at my form, which submits to the MVC action, "/Tickets/UpdateStatus":
<form asp-action="UpdateStatus" id="toTesting" class="updateStatus">
<input hidden name="ticketId" value="@Model.Id" />
<input hidden name="statusName" class="statusName" value="Testing" />
<button class="updateBtn btn btn-block btn-warning" type="submit">Update to Testing</button>
</form>
Here's the basic code used for the ajax request:
$(".updateStatus").on("submit", function (e) {
//SETUP
var dataString = $(this).serialize();
var newStatus = $(this).children(".statusName")[0].value
//AJAX Function
$.ajax({
type: "POST",
url: "/Tickets/UpdateStatus",
data: dataString,
//Result Area
success: function (result) {
toastr.success(`Ticket Status was updated`)
},
error: function (result) {
toastr.danger("Something went wrong, status wasn't updated")
});
//While We wait
e.preventDefault();
toastr.info('Updating Ticket Status. Please Wait')
});
Pay attention to the comments I left, I'll be referring to them as we enhance the call to interact with our page.
First off, I strongly recommend using toastr for easy toasts, The three lines you see (plus a script import tag) are all I had to write for a simple toasting service that showed the user temporary alerts.
You'll notice that this function seems a bit out of order, Lets walk through it real quick. At the top, we get any of the elements with the updateStatus class, and tell them to perform this function on submit. For the moment, Lets skip over the block beginning with $.ajax(
and look at the "while we wait" section. This is what happens on the page when our request is made, but before a response comes back from the server. The important thing is e.preventDefault();
which stops the form from being submitted as normal, which would refresh the page. Then we use Toastr to tell the user to hold on a moment.
When the result comes back from our ajax call, if its successful, we show the user a success toast, if there was an error, we inform them.
We now have a button that updates our ticket to testing, and it works, but we can make it work better and look better as well.
One at a time please
Some users (I've certainly been guilty) are impatient, and will click the button over and over if they don't think its working, despite what your please wait message says. Why not prevent that by disabling the button while the request is processed?
My button has the class updateBtn
so jQuery can find it. In the setup part of the function, lets add $(".updateBtn").attr("disabled", true)
so that the button will be disabled after the form is submitted. Once we get a success or error, we want to re-enable that button, so in both properties, I'm adding the following line to our function: $(".updateBtn").attr("disabled", false)
Now your form can't be submitted again until one request has either succeeded or returned an error, despite users who have an itchy trigger finger.
Changing the button
I'm going to come clean here, there is more than one button on this page, but theres no need to see them all at once.
The four buttons that are on my page are
<form asp-action="UpdateStatus" id="toTesting" class="updateStatus">
<input hidden name="ticketId" value="@Model.Id" />
<input hidden name="statusName" class="statusName" value="Testing" />
<button class="updateBtn btn btn-block btn-warning" type="submit">Update to Testing</button>
</form>
<form asp-action="UpdateStatus" id="returnTesting" class="updateStatus">
<input hidden name="ticketId" value="@Model.Id" />
<input hidden name="statusName" class="statusName" value="Development" />
<button class="updateBtn btn btn-block btn-danger" type="submit">Testing Failed: Return to Development</button>
</form>
<form asp-action="UpdateStatus" id="closeTicket" class="updateStatus">
<input hidden name="ticketId" value="@Model.Id" />
<input hidden name="statusName" class="statusName" value="Resolved" />
<button class="updateBtn btn btn-block btn-success mt-2" type="submit">Close Ticket</button>
</form>
<form asp-action="UpdateStatus" id="reopenTicket" class="updateStatus">
<input hidden name="ticketId" value="@Model.Id" />
<input hidden name="statusName" class="statusName" value="Development" />
<button class="updateBtn btn btn-block btn-success" type="submit">Reopen Ticket to Development status</button>
</form>
But we only need to see some of these at a time.
I wrote a small js function to handle this using Jquery's fadeIn and fadeOut methods (originally I used .show and .hide, but this produced a strange transition where things grew from the top left corner.) You just pass in the new status name and the time in ms for the fade to happen.
(ticketstatus is defined with my C# model: var ticketStatus = "@Model.TicketStatus.Name"
)
var toTesting = $("#toTesting")
var returnTesting = $("#returnTesting")
var closeTicket = $("#closeTicket")
var reopenTicket = $("#reopenTicket")
function statusButtons(status, num) {
if (status == "Development") {
toTesting.fadeIn(num)
returnTesting.fadeOut(num)
reopenTicket.fadeOut(num)
closeTicket.fadeOut(num)
}
else if (status == "Testing") {
toTesting.fadeOut(num)
returnTesting.fadeIn(num)
reopenTicket.fadeOut(num)
closeTicket.fadeIn(num)
}
else if (status == "Resolved") {
toTesting.fadeOut(num)
returnTesting.fadeOut(num)
reopenTicket.fadeIn(num)
closeTicket.fadeOut(num)
}
else{
toTesting.fadeOut(num)
returnTesting.fadeOut(num)
reopenTicket.fadeOut(num)
closeTicket.fadeOut(num)
}
}
statusButtons(ticketStatus,0)
I use Ids to reference each button, and then based on the status, we fade certain ones in or out.
We then call this right after definition, with a fade time of 0, to ensure that only the correct buttons are shown.
I then call this function when the successful result happens. If there was an error, we never changed our data, so theres no need to call the function.
success: function (result) {
toastr.success(`Ticket Status was updated`)
ticketStatus = newStatus
$("#ticketStatusText").text(ticketStatus)
statusButtons(ticketStatus, 600)
$(".updateBtn").attr("disabled", false)
},
newStatus comes from the hidden input within the form.
That should get you started making ajax calls without reloading the page, and give you the oppourtunity to expand. Take a look at the finished result on a ticket details page at the Jameson Bug Tracker (Log in as a demo project manager and then click on any ticket's details)
Let me know what you think in the comments!
Posted on June 21, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
May 1, 2023