In part 3 of this series we saw how to add and remove items in a list using server roundtrips. This (b) part shows an alternative implementation minimizing server roundtrips.
Introduction
In part 3 we have seen how to update a page containing a list to which we want to add or remove elements. We basically had an action method to add an item to the list (AddPassenger
) and an action method to remove an item from the list (RemovePassenger
).
Both action methods required a round-trip to the server, re-rendering the list with an extra set of fields for the new passenger, or a set of fields removed.
In this part 3b article, we will see how we can achieve similar behaviour without roundtripping to the server. For this you need at least version 2.4.0 of Sircl. Make sure to upgrade Sircl to this or to the latest version if you depart from the previous code.
Limitations
There are some limitations to take into account. The main one, is that there should be no dynamic content other than the user input on the different list elements.
We do have an issue here with the original flight registration form: the list shows the passenger number (1, 2, 3...). This is dynamic content computed by the server. If we eliminate the roundtrip to the server, we also eliminate the capability to render server-computed content. So we will replace the label “Passenger 1” by just “Passenger”.
Another issue we have is tied to ASP.NET (MVC) model binding. The binding to properties of objects in a list require the fieldname to contain the index of the element (0, 1, 2,...), again, this is a dynamic content computed by the server.
The exact behavior and syntax of model binding depends on the server side technology you are using and is out of scope of Sircl. But it is a point of attention.
In our example, I have refactored the passenger list from a list of Passenger objects into two lists of strings (one list for the first names and one for the last names). It is not ideal, but that is not the subject of this article...
Having covered the limitations, let’s now focus on the solution.
Removing items
Removing items from the list, in casu passengers from the flight, is pretty easy in HTML: just cut out the HTML code that stands for that list item. To remove HTML in reaction to a click event, with Sircl we use the onclick-remove
event-action attribute. The value attribute is a CSS selector to the element to remove.
Now, given that all list entries have identical HTML, how to identify the element to remove ? Giving each entry a unique id would again have required server-side computation.
The solution resides in the use of relative CSS selectors. This is an addition of Sircl. It basically allows to select elements by their relative position towards the element referring to them. And it is a capability that makes sense within Sircl, a HTML extension that uses inline CSS selectors.
Using a relative CSS selector, the button to remove the HTML code matching the current passenger entry can refer to the parent list entry element it is part of.
Consider the following code representing a passenger entry in the list:
<div class="mb-3">
<span class="float-end">
<button type="button" class="btn btn-sm btn-light onclick-setchanged" onclick-remove="<DIV" title="Remove this passenger">×</button>
</span>
<p><b>Passenger:</b></p>
<div class="row">
<div class="col">
<input name="Flight.PassengerFirstNames" value="" class="form-control" placeholder="First name" required>
</div>
<div class="col">
<input name="Flight.PassengerLastNames" value="" class="form-control" placeholder="Last name" required>
</div>
</div>
</div>
The onclick-remove
attribute’s value is “<DIV
”. This is a relative CSS selector: the “<
” arrow tells to look from the current element upward to the closest match for the remaining expression. In other words, this matches the closest parent DIV
element. That is the element to remove when the user clicks the “x” button.
Change Detection
In the previous article (part 3), we have added a change detection feature allowing our form to know when it’s data has changed. It relied on the change event and on the server setting a HasChanges
property on the model.
But removing HTML by clicking a button does not trigger a change event on the form. And we just eliminated the server roundtrip. Yet, the form has changed: there is one passenger less.
It is easy to solve however: to have the form being aware of change when the “x” button is clicked, we can add the onclick-setchanged
event-action class on the button.
Adding items
Adding items to the list without server roundtrip is a little trickier. When a passenger is to be added to the list, HTML code needs to be added to the form. When this HTML code is not coming from the server, where will it come from ?
To solve this we can let the server prepare the HTML code in advance and have it in the form on it’s initial rendering. We can place the code in a TEMPLATE
element. This has three consequences:
- The HTML code is available client-side, there is no need to roundtrip to the server to get it
- HTML code within a
TEMPLATE
element is not rendered
- And even though the HTML code is inside the
FORM
element and may contain required fields (or other validation restrictions), being inside a TEMPLATE
element means its validation attributes are ignored
So we can safely use a TEMPLATE
element and it does not matter if that element is inside or outside of the form. Which is nice, because it means we can place the template next to where it is to be used.
In the code below, the template is just after the button that refers to it.
But now, how do we use the template to add a passenger to the list ?
We still have a button to add a passenger. Only this time the button is not a submit button that causes a server roundtrip, but a mere button that holds Sircl event-actions. What you would need on that button is an event-action saying to append the content of the template to the list of passengers. So we need to refer (i.e. using a (relative or absolute) CSS selector) to the template as well as to the list. Unfortunately HTML attributes can have only a single value, and Sircl adheres to this limitation for the sake of simplicity.
So we will need tome trickery...
If we place the event-action attribute on the template itself, we only need to refer to the list, since the template is already known: it’s the current element. So on the template we add the onclick-appendto="#passengerlist"
attribute. This tells to append the content of the template to the content of the element with id “passengerlist
”.
But TEMPLATE
elements are hidden and can’t be clicked. However that does not mean templates can’t handle click events. So now we only need to send a click event to the template: we only need to refer to one item anymore: the template. Now we can use the onclick-click="#passengertemplate"
event-action attribute on the button. This tells the button that when it is clicked, it should trigger a click event on the element with id “passengertemplate
”: the template.
And we know that when the template gets the click event, it will append it’s content to the content of the passengerlist.
What we have done is called chaining event-actions, and it usually envolves click events since most Sircl actions can be triggered from a click event.
In addition, on the Add button, we also added the onclick-setchanged
class so the form knows it has changed.
Last but not least, when adding a passenger, we can set the focus on the First Name field of the newly added line. It could be done with an onclick-focus
attribute on the Add passenger button, given a CSS selector to select the First Name field of the last passenger as in:
onclick-focus="#passengerlist > *:last-child INPUT[name='Flight.PassengerFirstNames']"
but there’s an easier way: add an autofocus
attribute on the First Name field in the template. Whenever the template is copied, it’s autofocus
will be triggered.
The list of passengers now looks like:
<fieldset>
<legend>Passengers</legend>
<div id="passengerlist">
@for (int i = 0; i < Model.Flight.PassengerFirstNames.Count; i++)
{
<div class="mb-3">
<span class="float-end">
<button type="button" class="btn btn-sm btn-light onclick-setchanged" onclick-remove="<DIV" title="Remove this passenger">×</button>
</span>
<p><b>Passenger:</b></p>
<div class="row">
<div class="col">
<input name="Flight.PassengerFirstNames" value="@(Model.Flight.PassengerFirstNames[i])" class="form-control" placeholder="First name" required>
</div>
<div class="col">
<input name="Flight.PassengerLastNames" value="@(Model.Flight.PassengerLastNames[i])" class="form-control" placeholder="Last name" required>
</div>
</div>
</div>
}
</div>
<div class="mb-3">
<button type="button" class="btn btn-sm btn-secondary onclick-setchanged" onclick-click="#passengertemplate">
Add passenger
</button>
</div>
<template id="passengertemplate" onclick-appendto="#passengerlist" onload-copyto="@(Model.Flight == null ? "#passengerlist" : null)">
<div class="mb-3">
<span class="float-end">
<button type="button" class="btn btn-sm btn-light onclick-setchanged" onclick-remove="<DIV" title="Remove this passenger">×</button>
</span>
<p><b>Passenger:</b></p>
<div class="row">
<div class="col">
<input name="Flight.PassengerFirstNames" value="" class="form-control" placeholder="First name" required autofocus>
</div>
<div class="col">
<input name="Flight.PassengerLastNames" value="" class="form-control" placeholder="Last name" required>
</div>
</div>
</div>
</template>
</fieldset>
Client-side Validation
Another way to limit server roundtrips is by implementing client-side validation whenever possible. In this example I have used the required
attribute of HTML5 Form Validation making sure no server roundtrips occures if a required field is empty.
Server-side validation is still in place and will for instance ensure that the booking has at least one passanger.
Controllers
The full source code of this version can be downloaded. Have a look at the HomeController
and campare with the version of part 3: see how the AddPassenger
and RemovePassenger
action methods are gone, and how the code was changed to cope with the refactored passenger list data.