Angular / Basic / Lesson 6
Forms and accessible interaction
Use a typed form model with explicit validation and native labels, keeping user feedback separate from server trust and authorization.
On this page
Lesson goals
- Create non-nullable controls and explain reset behavior.
- Connect reactive controls through the correct standalone import.
- Present errors and submission behavior accessibly.
A form model makes value and validity explicit
Reactive forms represent controls, validation, and state in TypeScript, while template-driven forms use a different directive-centered approach. This lesson chooses reactive forms rather than mixing both models on one input. FormControl tracks value, validity, touched, and other interaction state. A typed control still needs runtime validators and server-side checks for submitted data. nonNullable changes the controlโs reset behavior as well as its TypeScript type; it is not a promise that every external caller supplies valid text. Decide the initial value, accepted length, and failure feedback before wiring the DOM.
Test the real control without claiming a browser interaction
The complete program imports the pinned compiler support and forms package in an isolated Node environment. It creates an empty non-nullable control with required and minimum-length validators. Setting Mira makes it valid, and reset returns the original empty string rather than null. These are actual form-model observations, not simulated keyboard input or visual error checks. Keep compiler support explicit for this local partially compiled package setup. A production Angular application follows its own build pipeline and should not copy a testing import blindly into every component. DOM directives and labels need a separate component test.
import "@angular/compiler";
import { FormControl, Validators } from "@angular/forms";
const name = new FormControl("", { nonNullable: true, validators: [Validators.required, Validators.minLength(3)] });
console.log(name.valid);
name.setValue("Mira");
console.log(name.valid, name.value);
name.reset();
console.log(name.valid, JSON.stringify(name.value));
false
true Mira
false ""
The lines are false, true Mira, and false with an empty JSON string. No form element, HTTP request, or browser is created.
Import the directives and connect a visible label
A standalone component using [formControl] needs ReactiveFormsModule in its imports. The contextual fragment assumes a name FormControl and a save method that rejects invalid submission. A native label points to the input id, and the error description is associated with aria-describedby. Show the error after an appropriate interaction such as touch or submission, rather than announcing failure before the user begins. A submit button and ngSubmit make keyboard submission part of the contract. The fragment does not demonstrate that assistive technology actually announces the message; that needs suitable manual and automated verification.
<form (ngSubmit)="save()" novalidate>
<label for="course-name">Course name</label>
<input id="course-name" [formControl]="name" aria-describedby="name-help">
<p id="name-help">Use at least three characters.</p>
@if (name.invalid && name.touched) {
<p role="status">Enter a course name of at least three characters.</p>
}
<button type="submit">Save</button>
</form>
With the declared control and imports, touching an empty input would reveal the validation message. The host class and browser interaction are not executed here.
Submission is a domain operation, not a validity flag alone
The handler should mark relevant controls as touched when invalid, return without saving, and construct a validated request when valid. Disabled controls can be omitted from a FormGroup value, while getRawValue includes their values; choose deliberately rather than assuming value always has every field. A pending async validator and a request in progress need their own policy to prevent duplicate work. Do not interpret a disabled button as authorization or a substitute for server validation. Preserve user input after a recoverable server error and explain the next action. Distinguish accepted submission from successful remote persistence.
Test value, error, keyboard, and focus behavior
Test initial validity, accepted values, boundaries, reset, invalid submission, and server rejection with synthetic data. Component tests should query the control by its accessible label, enter text, and submit through the user-facing path. A direct setValue test does not prove the input directive or label is wired correctly. Check that error messages are understandable, not conveyed by color alone, and do not trap focus. For dynamic forms, preserve stable control identity when rows change. Accessibility work needs keyboard and assistive-technology review in addition to structural checks; a valid FormControl model is only one layer of the complete interaction.
Common errors and fixes
The template cannot bind to formControl.
- Why it happens
- ReactiveFormsModule is missing from the owning standalone component.
- How to fix it
- Import it at that component boundary.
- Prevent it next time
- Test the real template rather than only the control class.
Reset unexpectedly yields null.
- Why it happens
- The control used nullable reset semantics.
- How to fix it
- Choose nonNullable when reset-to-initial-value is intended.
- Prevent it next time
- Include reset behavior in the form contract.
A valid client form is trusted by the server.
- Why it happens
- Presentation validation was treated as a trust boundary.
- How to fix it
- Validate and authorize the actual server request.
- Prevent it next time
- Test malformed and unauthorized submissions separately.
Check your understanding
1. Set the local control to "Al", then "Lina". Give validity results and explain what nonNullable does not guarantee.
Show worked solution
Al fails the minimum length and Lina passes both validators. nonNullable ensures this control resets to its initial string instead of null; it does not make every string valid or enforce server permission. Add these boundary cases to the model test, then verify that the component presents an appropriate error after the chosen interaction.
2. A FormGroup has a disabled identifier field that must accompany a save request. Explain how value and getRawValue differ and what remains necessary.
Show worked solution
The group value can omit disabled controls, while getRawValue includes them. Choose an explicit request construction that includes only intended fields, then validate and authorize the identifier on the server. A disabled client field is still client-controlled data and must not be treated as trusted ownership evidence. Test the actual submitted object keys.
Key takeaways
- Model form value and validity explicitly.
- Connect directives, labels, and submission deliberately.
- Client validation and server trust remain separate.