Forms are the way people tell a website something: a search, a sign-up, an appointment booking. An HTML form works entirely without JavaScript. The browser collects the fields, checks the simplest rules and sends everything to the server with one HTTP request. This is exactly how the Kladde builds its public appointment booking: It works in every browser, even if a script doesn’t load, and script at most makes it more convenient later (module 2.2).
In this lesson you build the third page of the prototype, appointment.html, and send it to a small server that shows you what arrives.
The form element
<form action="/book" method="post">
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
<button type="submit">Request appointment</button>
</form>
actionis the address the browser sends the data to. If it’s missing, the data goes to the address of the page itself.methodisgetorpost. If it’s missing,getapplies.- A field is only sent if it has a
nameattribute. Thenameis the key under which the server finds the value. Theid, on the other hand, is needed only by the page itself, for example for the label. button type="submit"submits the form. So does the Enter key in a text field. Always write outtype: Abuttonwithouttypeinside a form is a submit button, even if it was meant to do something else.
Labels
Every field needs a visible label, a label. The label’s for names the field’s id. This brings three things:
- When entering the field, a screen reader reads the label aloud: “Name, edit text, required”.
- A click on the label puts the focus in the field. For small checkboxes and radio buttons that is a much bigger click target.
- Voice control works: “Click Name”.
The placeholder (placeholder) is not a substitute for a label. It disappears while typing, is usually pale and hard to read, and
anyone who scrolls back no longer knows what belonged in the field. A hint for the field stands as a text of its own below it and is
connected with aria-describedby, so that a screen reader reads it after the label:
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="email" required aria-describedby="email-hint">
<p id="email-hint">We’ll send the confirmation to this address.</p>
Input types
The type attribute tells the browser what kind of value is expected. On the phone you then get the matching keyboard, and the browser
checks simple formats itself:
| Element or type | What for | What the browser does |
|---|---|---|
type="text" |
free text, such as a name | nothing special |
type="email" |
an email address | keyboard with @, roughly checks the format |
type="tel" |
a phone number | number keyboard, checks nothing, because formats differ around the world |
type="number" |
a quantity you calculate with | arrows to count up and down; not for ZIP codes or phone numbers, which keep their leading zeros |
type="date", type="time" |
date, time | a picker in a calendar; what’s sent is always YYYY-MM-DD and HH:MM, however it’s displayed |
type="checkbox" |
yes or no, several out of many | sent only if checked |
type="radio" |
exactly one out of a few | all with the same name form a group |
type="password" |
a password | shows dots instead of characters |
select with option |
one out of many | a drop-down list |
textarea |
longer text | a multi-line field |
So a date always arrives at the server the same way, even if a US browser displays 12/02/2025 and a German one 02.12.2025.
Required fields and limits
With attributes you tell the browser what is valid:
required: The field must not be empty.minlength,maxlength: length of text.min,max,step: limits for numbers, dates and times.pattern: a regular expression that the whole value must satisfy. Use it sparingly. A pattern that’s too strict for names or phone numbers locks out real people.
If a value is invalid, the browser doesn’t submit the form, puts the focus on the first faulty field and shows a short message. Its wording and look depend on the browser.
This is a convenience, not security. Anyone can send a server whatever they like: with curl, with another program or with a form
from which they’ve deleted the required in the browser. So the server checks every input again itself. In the Kladde, validation at
the edge takes care of that from module 2.3 on, and domain rules like “no double booking” are checked by the model.
With the attribute novalidate on the form you switch off the browser’s check. You need that for exercise 2, to see what the server
does with empty fields.
autocomplete: the browser helps fill in
With autocomplete you tell the browser what information a field expects. It then offers to fill it in from what it has stored. That
saves everyone typing and helps especially people for whom typing is hard. The most important values:
| Value | Meaning |
|---|---|
name |
full name |
email |
email address |
tel |
phone number |
organization |
company |
street-address, postal-code, address-level2 |
street, ZIP code, city |
current-password, new-password |
password when signing in and when creating one |
off |
don’t fill in; only for fields where a stored value would be wrong, such as a one-time code |
The values are fixed; no browser understands autocomplete="e-mail". For information about the person filling in the form, the
accessibility rules even require this labeling (lesson 2.1.7).
Groups with fieldset and legend
If several fields belong together, put them in a fieldset with a legend as its heading. For radio buttons this is a must for
comprehension: Without a group you hear only “Advice, radio button, one of two” and don’t know what’s being asked. With a group you hear
the question first.
<fieldset>
<legend>What is it about?</legend>
<div>
<input type="radio" id="topic-advice" name="topic" value="advice" checked>
<label for="topic-advice">Advice on a project</label>
</div>
<div>
<input type="radio" id="topic-help" name="topic" value="help">
<label for="topic-help">Help with a problem</label>
</div>
</fieldset>
Both radio buttons have the same name, so exactly one value belongs to the key topic. What’s sent is the value of the selected one,
not its label. The checked preselects an option so that the group is never submitted empty.
The form server
So that you can see what arrives at the server, there is a second practice server. It serves the files of your prototype, so that the
pages appear in the browser at http://localhost:5080/, and has four endpoints:
/echoshows every received field as text, forGETandPOST;POST /bookchecks the required fields, books and redirects with303to the confirmation;POST /book-directbooks without checking and answers immediately with200, only for comparison in exercise 3;GET /bookings/{number}shows a booking.
Create form-server.cs in your practice folder k2-practice:
// form-server.cs: serves the prototype and shows what a form sends
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using System.Text;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
string folder = Path.GetFullPath(args.Length > 0 ? args[0] : "prototype");
if (!Directory.Exists(folder))
{
Console.WriteLine($"The folder {folder} doesn't exist.");
return 2;
}
var builder = WebApplication.CreateBuilder();
builder.Logging.SetMinimumLevel(LogLevel.Warning);
var app = builder.Build();
var files = new PhysicalFileProvider(folder);
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = files });
app.UseStaticFiles(new StaticFileOptions { FileProvider = files });
// Shows every field that arrives, as text.
app.MapMethods("/echo", ["GET", "POST"], async (HttpRequest request) =>
{
var text = new StringBuilder();
text.AppendLine($"Method: {request.Method}");
text.AppendLine($"Content-Type: {request.ContentType ?? "(none)"}");
text.AppendLine($"Query: {(request.QueryString.HasValue ? request.QueryString.Value : "(none)")}");
IEnumerable<KeyValuePair<string, StringValues>> fields =
request.HasFormContentType ? await request.ReadFormAsync() : request.Query;
foreach (var (name, values) in fields)
{
foreach (string? value in values)
{
text.AppendLine($"{name} = {(string.IsNullOrEmpty(value) ? "(empty)" : value)}");
}
}
return Results.Text(text.ToString(), "text/plain; charset=utf-8");
});
List<string> bookings = [];
string[] required = ["name", "email", "day", "time"];
int Book(IFormCollection form)
{
lock (bookings)
{
bookings.Add($"{form["name"]} <{form["email"]}>, {form["day"]} at {form["time"]}");
return bookings.Count;
}
}
// Checks the required fields, books and redirects with 303 to the confirmation (Post-Redirect-Get).
app.MapPost("/book", async (HttpRequest request, HttpResponse response) =>
{
var form = await request.ReadFormAsync();
string[] missing = [.. required.Where(field => string.IsNullOrWhiteSpace(form[field]))];
if (missing.Length > 0)
{
return Results.Text($"Missing: {string.Join(", ", missing)}", "text/plain; charset=utf-8", statusCode: 400);
}
int id = Book(form);
response.Headers.Location = $"/bookings/{id}";
return Results.StatusCode(StatusCodes.Status303SeeOther);
});
// Books without checking and answers at once with 200, no redirect: for comparison, not like this.
app.MapPost("/book-direct", async (HttpRequest request) =>
{
int id = Book(await request.ReadFormAsync());
return Results.Text($"Booked as number {id}. So far {bookings.Count} booking(s).", "text/plain; charset=utf-8");
});
app.MapGet("/bookings/{id:int}", (int id) =>
id >= 1 && id <= bookings.Count
? Results.Text($"Booking {id}: {bookings[id - 1]}. So far {bookings.Count} booking(s).", "text/plain; charset=utf-8")
: Results.NotFound());
Console.WriteLine($"The form server is serving {folder} at http://localhost:5080. Quit with Ctrl+C.");
app.Run("http://localhost:5080");
return 0;
A few things in it you know from lesson 2.1.1; new are:
UseDefaultFilesandUseStaticFilesserve files from a folder. The first makes sure that/shows the fileindex.html.request.ReadFormAsync()reads the fields of a submitted form,request.Querythe fields of a query.- There’s no ready-made method for
303in minimal APIs; the endpoint setsLocationitself and answers with the status code. - As in lesson 2.1.1, the server forgets everything when it quits.
Start it with the folder of your prototype as the argument (adjust the path if your Kladde is somewhere else):
dotnet run form-server.cs -- ../kladde/prototype
The form server is serving /home/ada/kladde/prototype at http://localhost:5080. Quit with Ctrl+C.
On your machine, the full path of your folder is shown there. Open http://localhost:5080/ in the browser: The prototype’s home page
appears, and the links work like on a real server.
What arrives at the server: GET and POST
If a form submits with method="get", the browser appends the fields to the address as a query. You can reproduce the same with curl:
curl "http://localhost:5080/echo?topic=help&day=&time=09%3A00&name=Ada+Sample"
Method: GET
Content-Type: (none)
Query: ?topic=help&day=&time=09%3A00&name=Ada+Sample
topic = help
day = (empty)
time = 09:00
name = Ada Sample
A form encodes spaces as + and the colon as %3A; the server turns that back into the value. An empty field is sent along, just empty.
With method="post", the same fields are in the body of the request, in the format application/x-www-form-urlencoded, and the address
stays clean. --data-urlencode sends them the way a browser would:
curl --data-urlencode "topic=advice" --data-urlencode "day=2025-12-02" --data-urlencode "time=10:00" --data-urlencode "name=Ada Sample" --data-urlencode "email=ada@example.com" --data-urlencode "phone=" --data-urlencode "message=It's about Feld & Partner." http://localhost:5080/echo
Method: POST
Content-Type: application/x-www-form-urlencoded
Query: (none)
topic = advice
day = 2025-12-02
time = 10:00
name = Ada Sample
email = ada@example.com
phone = (empty)
message = It's about Feld & Partner.
When to use which method?
- GET for everything that only reads: a search, a filter, a page of a list. The result has an address of its own; you can save it as a bookmark, share it and find it again with “Back”. The Kladde’s contact search will be such a form (module 2.2).
- POST for everything that changes or creates something: a booking, a sign-up, a message. And for everything that doesn’t belong in an address: Addresses end up in the browser’s history, in bookmarks and in the logs of servers. Passwords and personal details have no business there.
A third format you know here only by name: Anyone who uploads files needs enctype="multipart/form-data" on the form. Then each field
arrives as a part of its own.
Error messages at the field
If the server checks an input and finds an error, it sends back the page with the form: with everything the person has already entered, and with a message at the field. A good error page has four features:
- The title begins with “Error:”, so that a screen reader says it right when the page loads.
- At the top is a list of all the errors, each one a link to its field.
- At the field, the message is there as text. It says what to do, not just what’s wrong: “The email address needs an @, for example
name@example.com” instead of “Invalid input”. - The field carries
aria-invalid="true"and points witharia-describedbyto its message. A screen reader then reads “Email, invalid entry, The email address needs an @ …”.
This is what the field looks like in the error case:
<label for="email">Email</label>
<input type="email" id="email" name="email" value="ada.example.com" autocomplete="email" required
aria-invalid="true" aria-describedby="email-error email-hint">
<p id="email-error">The email address needs an @, for example name@example.com.</p>
<p id="email-hint">We'll send the confirmation to this address.</p>
aria-describedby may name several id values, separated by spaces; the screen reader reads them in that order. And a color alone is
not an error message: A field with a red outline but no text is overlooked by anyone who doesn’t see red. In lesson 2.1.4 the message
additionally gets a color, and in module 2.5 Blazor generates this page for you.
Post-Redirect-Get
What happens if the server answers a submitted form directly with 200 and a confirmation page? The page in the browser is then the
result of a POST. If the person reloads it, say because the network was briefly gone, the browser asks “Resubmit form?”. Anyone who
clicks “Yes” books a second time. “Back” and “Forward” can trigger that too.
The Post-Redirect-Get pattern prevents that:
Browser Server
POST /book (name, email, day, time) ──▶ checks, books as number 7
◀── 303 See Other, Location: /bookings/7
GET /bookings/7 ──▶ reads booking 7
◀── 200 OK, the confirmation
After the POST, the server answers only with a redirect. The browser fetches the confirmation with GET, and the page in the browser
is now the result of this GET. Reloading fetches the confirmation again but books nothing. With curl you see both steps if you add
-L:
curl -i -L --data-urlencode "name=Ilse Kranich" --data-urlencode "email=ilse.kranich@example.net" --data-urlencode "day=2025-12-03" --data-urlencode "time=14:00" http://localhost:5080/book
HTTP/1.1 303 See Other
Content-Length: 0
Location: /bookings/1
HTTP/1.1 200 OK
Content-Length: 93
Content-Type: text/plain; charset=utf-8
Booking 1: Ilse Kranich <ilse.kranich@example.net>, 2025-12-03 at 14:00. So far 1 booking(s).
As in lesson 2.1.1, the Date and Server lines are left out, and the number depends on how often you’ve already booked.
A stumbling block with curl: Do not add -X POST here. -X forces curl to follow the redirect with POST too, and after a
second booking you get 405 Method Not Allowed. Without -X, curl turns a -d or --data-urlencode into a POST on its own and
follows the 303 with GET, like a browser.
If the server answers with an error, on the other hand, such as 400 because fields are missing, it does not redirect: The error
page isn’t a result you’d want to reload, and nothing was booked, after all.
Exercise 1: The booking page and its echo
Build prototype/appointment.html with the same header and footer as the other pages (aria-current="page" now on “Book an
appointment”), the title “Book an appointment – Kladde”, the heading “Book an appointment” and a sentence saying that an appointment
lasts one hour. Below it, a form with action="/echo" and method="post" and these fields:
- a group “What is it about?” with two options: Advice on a project (preselected) and Help with a problem;
- a group “When?” with the day (date, required) and the time (a choice of 09:00, 10:00, 11:00, 14:00, 15:00 and 16:00, required, with an empty first option “Please choose”);
- a group “Your details” with name (required, at most 100 characters), email (required, with the hint “We’ll send the confirmation to this address.”), phone (optional) and message (optional, at most 1000 characters);
- a checkbox “Remind me by email the day before the appointment”;
- a button “Request appointment”.
Fields that aren’t required carry the addition “(optional)” in their label. Name, email and phone get the fitting autocomplete.
Start the form server with your prototype and open http://localhost:5080/appointment.html. Before you submit: Write down which lines
the echo will show if you fill in everything but don’t check the checkbox. Then submit and compare. Repeat it with the checkbox
checked, and finally with method="get": What’s in the address bar now?
The exercise is checked when the validator reports nothing for appointment.html, outline.cs finds no problems and your prediction of
the echo was right or you can explain every deviation.
Solution 1
The form, here already with action="/book", as it stands in the Kladde at the end of the lesson. For this exercise it still says
/echo.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Book an appointment – Kladde</title>
</head>
<body>
<header>
<a href="index.html"><img src="logo.svg" alt="Kladde, home page" width="48" height="48"></a>
<nav aria-label="Main navigation">
<ul>
<li><a href="index.html">Home</a></li>
<li><a href="contacts.html">Contacts</a></li>
<li><a href="appointment.html" aria-current="page">Book an appointment</a></li>
</ul>
</nav>
</header>
<main>
<h1>Book an appointment</h1>
<p>An appointment lasts one hour. You have to fill in the fields without the addition “optional”.</p>
<form action="/book" method="post">
<fieldset>
<legend>What is it about?</legend>
<div>
<input type="radio" id="topic-advice" name="topic" value="advice" checked>
<label for="topic-advice">Advice on a project</label>
</div>
<div>
<input type="radio" id="topic-help" name="topic" value="help">
<label for="topic-help">Help with a problem</label>
</div>
</fieldset>
<fieldset>
<legend>When?</legend>
<div>
<label for="day">Day</label>
<input type="date" id="day" name="day" required>
</div>
<div>
<label for="time">Time</label>
<select id="time" name="time" required>
<option value="">Please choose</option>
<option>09:00</option>
<option>10:00</option>
<option>11:00</option>
<option>14:00</option>
<option>15:00</option>
<option>16:00</option>
</select>
</div>
</fieldset>
<fieldset>
<legend>Your details</legend>
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" autocomplete="name" required maxlength="100">
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="email" required aria-describedby="email-hint">
<p id="email-hint">We'll send the confirmation to this address.</p>
</div>
<div>
<label for="phone">Phone (optional)</label>
<input type="tel" id="phone" name="phone" autocomplete="tel">
</div>
<div>
<label for="message">Message (optional)</label>
<textarea id="message" name="message" rows="4" maxlength="1000"></textarea>
</div>
</fieldset>
<div>
<input type="checkbox" id="reminder" name="reminder" value="yes">
<label for="reminder">Remind me by email the day before the appointment</label>
</div>
<button type="submit">Request appointment</button>
</form>
</main>
<footer>
<p>Kladde, a practice project. All people and companies are made up.</p>
</footer>
</body>
</html>
The echo without the checkbox checked, with empty optional fields (your date will be a different one):
Method: POST
Content-Type: application/x-www-form-urlencoded
Query: (none)
topic = advice
day = 2025-12-02
time = 10:00
name = Ada Sample
email = ada@example.com
phone = (empty)
message = (empty)
The most common deviations from the prediction:
- The checkbox is missing entirely, instead of showing up with an empty value. An unchecked checkbox sends nothing. With a check, the
line
reminder = yesappears at the end, so thevalue, not the label. For the server, “not there” therefore means “no”. - The button doesn’t appear. It has no
name, so it sends nothing. If you gave it one, it would come along too. topicisadvice, not “Advice on a project”: What’s sent is thevalue.- The date arrives as
YYYY-MM-DD, however the browser displays it. - Empty optional fields come along, just empty.
With method="get", everything is in the address bar, for example
http://localhost:5080/echo?topic=advice&day=2025-12-02&time=10%3A00&name=Ada+Sample&email=ada%40example.com&phone=&message=.
That’s exactly why get is wrong for a booking: Name and email address would be in the browser’s history and in every server log.
Exercise 2: Submitting required fields empty
Let the form still send to /echo and proceed like this:
- Leave name and email empty and click “Request appointment”. What happens, and where does the focus jump?
- Write
ada.example.comin the email field and fill in everything else. What happens? - Put
novalidateon theformelement and submit with an empty name and an empty email. What does the echo show? - Change
actionto/bookand submit again with empty fields. Which status code do you get? Check it withcurltoo, entirely without a browser. - Take
novalidateout again.
For each step, note in one sentence who found the error: the browser or the server.
Solution 2
The browser sends nothing. It puts the focus in the first empty required field and shows a short message there. No request arrives at the server.
Again the browser:
type="email"demands an@. Check it: In the server’s terminal and in the echo, nothing happens.With
novalidate, the browser checks nothing, and the echo showsname = (empty)andemail = (empty). That’s the situation every server has to be ready for: It gets what is sent, not what the form asked for.The server answers with
400and the textMissing: name, email. Withcurl:curl -i --data-urlencode "name=" --data-urlencode "email=ada@example.com" http://localhost:5080/bookHTTP/1.1 400 Bad Request Content-Length: 24 Content-Type: text/plain; charset=utf-8 Missing: name, day, timeThe request doesn’t send
dayandtimeat all, and the server counts them as missing too. So it checks not only empty but also missing fields, and that’s what matters, because a home-made client doesn’t stick to any form.Without
novalidate, the browser checks again before submitting. Both together are right: the browser for convenience, the server for security.
Exercise 3: Experiencing Post-Redirect-Get
Restart the form server so that it begins with zero bookings.
- Set
actionto/book-directand book an appointment. The browser shows “Booked as number 1. So far 1 booking(s).” Now reload the page with F5 and confirm the browser’s prompt. What does it say? - Set
actionto/bookand book again. Which address is in the address bar afterward? Reload: Does a prompt appear, and does the number of bookings change? - Leave
action="/book"in place; that’s how it stays in the Kladde.
Solution 3
- After reloading, it says “Booked as number 2. So far 2 booking(s).” The browser repeated the
POST, and the server booked a second time. The address bar still says/book-direct. A person who reloads twice on a bad network would have three appointments. - The address bar shows
/bookings/3, and there it says “Booking 3: … So far 3 booking(s).” Reloading doesn’t ask and changes nothing: The browser merely repeats theGETof the confirmation, which is safe and idempotent (lesson 2.1.1).
Post-Redirect-Get doesn’t make submitting idempotent: Anyone who submits the form a second time themselves books a second time. It only prevents reloading and going back from doing so unnoticed. Against double bookings, a rule in the model and in the database helps in module 2.4.
Exercise 4: The page in the error case
Build prototype/appointment-error.html: This is what the booking page should look like when the server has found two errors, a missing
time and an email address without @. For this you need only the groups “When?” and “Your details” with day, time, name and email.
Follow the four features of a good error page from the section “Error messages at the field”. The person’s entries stand as value in
the fields. Check with the validator and outline.cs, and click the links of the error list in the browser: Does the focus jump into
the field each time?
This page is a template for later: In lesson 2.1.4 it gets its colors, in module 2.5 Blazor generates it.
Solution 4
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Error: Book an appointment – Kladde</title>
</head>
<body>
<header>
<a href="index.html"><img src="logo.svg" alt="Kladde, home page" width="48" height="48"></a>
<nav aria-label="Main navigation">
<ul>
<li><a href="index.html">Home</a></li>
<li><a href="contacts.html">Contacts</a></li>
<li><a href="appointment.html" aria-current="page">Book an appointment</a></li>
</ul>
</nav>
</header>
<main>
<h1>Book an appointment</h1>
<div>
<h2>Please check two details</h2>
<ul>
<li><a href="#time">Choose a time.</a></li>
<li><a href="#email">The email address needs an @, for example name@example.com.</a></li>
</ul>
</div>
<form action="/book" method="post">
<fieldset>
<legend>When?</legend>
<div>
<label for="day">Day</label>
<input type="date" id="day" name="day" value="2025-12-02" required>
</div>
<div>
<label for="time">Time</label>
<select id="time" name="time" required aria-invalid="true" aria-describedby="time-error">
<option value="">Please choose</option>
<option>09:00</option>
<option>10:00</option>
</select>
<p id="time-error">Choose a time.</p>
</div>
</fieldset>
<fieldset>
<legend>Your details</legend>
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" value="Ada Sample" autocomplete="name" required maxlength="100">
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" value="ada.example.com" autocomplete="email" required
aria-invalid="true" aria-describedby="email-error email-hint">
<p id="email-error">The email address needs an @, for example name@example.com.</p>
<p id="email-hint">We'll send the confirmation to this address.</p>
</div>
</fieldset>
<button type="submit">Request appointment</button>
</form>
</main>
</body>
</html>
The validator reports nothing, and outline.cs shows the outline h1 Book an appointment, below it h2 Please check two details. The
links of the error list point to the id of the fields; a click jumps there and puts the focus in the field. The message in the list and
at the field is worded identically, so that nobody has to compare two texts. The list of times is shortened here; on the real page all
the times are there.
The Kladde: the booking page
Commit prototype/appointment.html with action="/book" and prototype/appointment-error.html on a branch of their own, for example
prototype-booking, and open a pull request. In the description, write how you checked: validator, outline.cs, a run through the
booking with the form server up to the confirmation, a reload without a second booking.
Do not commit the form server; it’s a practice tool from your folder k2-practice. If you open the prototype without the server with
a double click, “Request appointment” leads nowhere. Only in module 2.5 does the Kladde get its own endpoint for the booking.
What you can do now
- You build a form with
action,method, labels and fitting input types. - You set
required, lengths andautocompleteand group withfieldsetandlegend. - You know that the browser checks only for convenience and that the server has to check every input itself.
- You choose
GETfor anything that reads and has an address of its own, andPOSTfor anything that changes something or doesn’t belong in an address. - You mark up errors at the field with text,
aria-invalidandaria-describedby. - You apply Post-Redirect-Get and can explain what it protects against and what it doesn’t.
Read more
- Build web pages with HTML and CSS for beginners: the Microsoft Learn learning path, with forms too.
- Static files in ASP.NET Core: how
UseDefaultFilesandUseStaticFilesserve files; you need this again in lesson 2.1.6. - Web forms at MDN: forms from the ground up, with all input types.
- The HTML attribute autocomplete at MDN: the complete list of values.