- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Label
- Menubar
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner (Toast)
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
Reactive Forms
Build forms in Angular using Reactive Forms.
In this guide, we will take a look at building forms with Reactive Forms. We'll cover building forms with the HlmField component, how to handle validation and how to display errors.
Demo
We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
Bug Report
Help us improve by reporting bugs you encounter.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmFieldImports } from '@spartan-ng/helm/field';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmInputGroupImports } from '@spartan-ng/helm/input-group';
@Component({
selector: 'spartan-reactive-forms-demo',
imports: [
ReactiveFormsModule,
HlmCardImports,
HlmFieldImports,
HlmInputImports,
HlmInputGroupImports,
HlmButtonImports,
],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'w-full sm:max-w-md' },
template: `
<hlm-card>
<hlm-card-header>
<h3 hlmCardTitle>Bug Report</h3>
<p hlmCardDescription>Help us improve by reporting bugs you encounter.</p>
</hlm-card-header>
<div hlmCardContent>
<form id="form-bug-report" [formGroup]="form" (ngSubmit)="submit()">
<hlm-field-group>
<hlm-field>
<label hlmFieldLabel for="title">Bug Title</label>
<input
hlmInput
id="title"
placeholder="Login button not working on mobile"
autoComplete="off"
formControlName="title"
/>
<hlm-field-error validator="required">Title must be entered.</hlm-field-error>
<hlm-field-error validator="minlength">Title must be at least 5 characters.</hlm-field-error>
<hlm-field-error validator="maxlength">Title cannot exceed 32 characters.</hlm-field-error>
</hlm-field>
<hlm-field>
<label hlmFieldLabel for="description">Description</label>
<hlm-input-group>
<textarea
hlmInputGroupTextarea
id="description"
class="min-h-24"
placeholder="I'm having an issue with the login button on mobile."
rows="6"
formControlName="description"
></textarea>
<hlm-input-group-addon align="block-end">
<span hlmInputGroupText>{{ form.controls.description.value?.length || 0 }}/100 characters</span>
</hlm-input-group-addon>
</hlm-input-group>
<hlm-field-description>
Include steps to reproduce, expected behavior, and what actually happened.
</hlm-field-description>
<hlm-field-error validator="required">Description must be entered.</hlm-field-error>
<hlm-field-error validator="minlength">Description must be at least 20 characters.</hlm-field-error>
<hlm-field-error validator="maxlength">Description cannot exceed 100 characters.</hlm-field-error>
</hlm-field>
</hlm-field-group>
</form>
</div>
<hlm-card-footer>
<hlm-field orientation="horizontal">
<button hlmBtn variant="outline" type="button" (click)="form.reset()">Reset</button>
<button hlmBtn type="submit" form="form-bug-report">Submit</button>
</hlm-field>
</hlm-card-footer>
</hlm-card>
`,
})
export class ReactiveFormsDemo {
private readonly _fb = inject(FormBuilder);
public form = this._fb.group({
title: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(32)]],
description: ['', [Validators.required, Validators.minLength(20), Validators.maxLength(100)]],
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('You submitted the following values:', JSON.stringify(this.form.value, null, 2));
}
}Approach
This form leverages Angular Reactive Form for flexible form handling. We'll build our form using the HlmField component, which gives you complete flexibility over the markup and styling.
- Uses Angular
FormBuilderfor the form state management and validation. HlmFieldcomponents for building accessible forms.formControlName="control"binds the form control to the input element.
Anatomy
Here's a basic example of a form using the HlmField component and binding it via formControlName to the input element.
Notice that HlmFieldLabel does not need a for attribute. When a label and a control are placed inside the same HlmField , the for attribute is automatically set to the control's id .
<hlm-field>
<label hlmFieldLabel>Bug Title</label>
<input
hlmInput
id="title"
placeholder="Login button not working on mobile"
autoComplete="off"
formControlName="title"
/>
<hlm-field-error validator="required">Title is a required field.</hlm-field-error>
<hlm-field-error validator="minlength">Title must be at least 5 characters.</hlm-field-error>
<hlm-field-error validator="maxlength">Title cannot exceed 32 characters.</hlm-field-error>
</hlm-field>Form
Create form controls
We'll start by defining the shape of our form using the form builder.
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'bug-report-form',
imports: [],
host: { class: 'w-full sm:max-w-md' },
template: `...`,
})
export class BugReportForm {
private readonly _fb = inject(FormBuilder);
public form = this._fb.group({
title: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(32)]],
description: ['', [Validators.required, Validators.minLength(20), Validators.maxLength(100)]],
});
}Setup the form
Next, we'll import ReactiveFormsModule and bind the form schema to the form element.
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'bug-report-form',
imports: [ReactiveFormsModule],
host: { class: 'w-full sm:max-w-md' },
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<!-- Build the form here -->
</form>
`,
})
export class BugReportForm {
private readonly _fb = inject(FormBuilder);
public form = this._fb.group({
title: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(32)]],
description: ['', [Validators.required, Validators.minLength(20), Validators.maxLength(100)]],
});
submit() {
// Do something with the form values.
console.log(this.form.value);
}
}Build the form
We can now build the form using HlmField component and bind the form controls using formControlName .
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmFieldImports } from '@spartan-ng/helm/field';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmInputGroupImports } from '@spartan-ng/helm/input-group';
@Component({
selector: 'spartan-reactive-forms-demo',
imports: [
ReactiveFormsModule,
HlmCardImports,
HlmFieldImports,
HlmInputImports,
HlmInputGroupImports,
HlmButtonImports,
],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'w-full sm:max-w-md' },
template: `
<hlm-card>
<hlm-card-header>
<h3 hlmCardTitle>Bug Report</h3>
<p hlmCardDescription>Help us improve by reporting bugs you encounter.</p>
</hlm-card-header>
<div hlmCardContent>
<form id="form-bug-report" [formGroup]="form" (ngSubmit)="submit()">
<hlm-field-group>
<hlm-field>
<label hlmFieldLabel for="title">Bug Title</label>
<input
hlmInput
id="title"
placeholder="Login button not working on mobile"
autoComplete="off"
formControlName="title"
/>
<hlm-field-error validator="required">Title must be entered.</hlm-field-error>
<hlm-field-error validator="minlength">Title must be at least 5 characters.</hlm-field-error>
<hlm-field-error validator="maxlength">Title cannot exceed 32 characters.</hlm-field-error>
</hlm-field>
<hlm-field>
<label hlmFieldLabel for="description">Description</label>
<hlm-input-group>
<textarea
hlmInputGroupTextarea
id="description"
class="min-h-24"
placeholder="I'm having an issue with the login button on mobile."
rows="6"
formControlName="description"
></textarea>
<hlm-input-group-addon align="block-end">
<span hlmInputGroupText>{{ form.controls.description.value?.length || 0 }}/100 characters</span>
</hlm-input-group-addon>
</hlm-input-group>
<hlm-field-description>
Include steps to reproduce, expected behavior, and what actually happened.
</hlm-field-description>
<hlm-field-error validator="required">Description must be entered.</hlm-field-error>
<hlm-field-error validator="minlength">Description must be at least 20 characters.</hlm-field-error>
<hlm-field-error validator="maxlength">Description cannot exceed 100 characters.</hlm-field-error>
</hlm-field>
</hlm-field-group>
</form>
</div>
<hlm-card-footer>
<hlm-field orientation="horizontal">
<button hlmBtn variant="outline" type="button" (click)="form.reset()">Reset</button>
<button hlmBtn type="submit" form="form-bug-report">Submit</button>
</hlm-field>
</hlm-card-footer>
</hlm-card>
`,
})
export class ReactiveFormsDemo {
private readonly _fb = inject(FormBuilder);
public form = this._fb.group({
title: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(32)]],
description: ['', [Validators.required, Validators.minLength(20), Validators.maxLength(100)]],
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('You submitted the following values:', JSON.stringify(this.form.value, null, 2));
}
}Done
That's it. You now have a fully accessible form with client-side validation.
When you submit the form, the ngSubmit emits an event. Check if the form data is form.invalid and use form.markAllAsTouched() to mark all controls as touched. This will trigger the display of validation errors next to each field.
Validation
Reactive Forms provides a powerful validation system. Validator functions support either synchronous or asynchronous validation. You can use built-in validators or create custom ones. Built-in validators are available in the Validators class such as Validators.required or Validators.email .
Add one or more validator functions to your form schema using the form builder. If the form data is invalid, you can check the form's validity using form.valid and form.invalid .
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'bug-report-form',
imports: [],
host: { class: 'w-full sm:max-w-md' },
template: `...`,
})
export class BugReportForm {
private readonly _fb = inject(FormBuilder);
public form = this._fb.group({
title: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(32)]],
description: ['', [Validators.required, Validators.minLength(20), Validators.maxLength(100)]],
});
}Displaying Errors
HlmFieldError renders automatically when placed inside an HlmField whose control is invalid. There is no need to wrap it in @if blocks or manually check control state — the field handles visibility for you.
Visibility is driven by the data-matches-spartan-invalid attribute, which spartan sets on the field element. Its value reflects the return value of ErrorStateMatcher.isInvalid() — by default true when the control is invalid and touched. This attribute controls both error styling and the visibility of HlmFieldError messages.
You can change when errors appear by providing a custom ErrorStateMatcher at the component or application level. spartan ships with ShowOnDirtyErrorStateMatcher as a ready-made alternative that shows errors as soon as the control is dirty rather than waiting for a touch event.
When a validator input is specified on HlmFieldError , the message is only shown when the control has an error matching that validator key — letting you place multiple targeted error messages inside a single HlmField , each tied to a specific validation rule.
<hlm-field>
<label hlmFieldLabel>Bug Title</label>
<input
hlmInput
id="title"
placeholder="Login button not working on mobile"
autoComplete="off"
formControlName="title"
/>
<hlm-field-error validator="required">Title is a required field.</hlm-field-error>
<hlm-field-error validator="minlength">Title must be at least 5 characters.</hlm-field-error>
<hlm-field-error validator="maxlength">Title cannot exceed 32 characters.</hlm-field-error>
</hlm-field> Set forceShow on HlmFieldError to always display the message, regardless of the control's error state or the active ErrorStateMatcher . This is useful for cross-field validation errors that are not tied to a specific control, such as a password mismatch message.
<hlm-field>
<label hlmFieldLabel for="title">Bug Title</label>
<input hlmInput id="title" formControlName="title" />
<hlm-field-error forceShow>This error is always visible.</hlm-field-error>
</hlm-field>Working with Different Field Types
Input
Profile Settings
Update your profile information below.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmFieldImports } from '@spartan-ng/helm/field';
import { HlmInputImports } from '@spartan-ng/helm/input';
@Component({
selector: 'spartan-reactive-form-input-demo',
imports: [ReactiveFormsModule, HlmCardImports, HlmFieldImports, HlmInputImports, HlmButtonImports],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'w-full sm:max-w-md' },
template: `
<hlm-card>
<hlm-card-header>
<h3 hlmCardTitle>Profile Settings</h3>
<p hlmCardDescription>Update your profile information below.</p>
</hlm-card-header>
<div hlmCardContent>
<form id="form-input-demo" [formGroup]="form" (ngSubmit)="submit()">
<hlm-field-group>
<hlm-field>
<label hlmFieldLabel for="username">Username</label>
<input hlmInput id="username" placeholder="spartan" autoComplete="username" formControlName="username" />
<hlm-field-description>
This is your public display name. Must be between 3 and 10 characters. Must only contain letters,
numbers, and underscores.
</hlm-field-description>
<hlm-field-error validator="required">Username is a required field.</hlm-field-error>
<hlm-field-error validator="minlength">Username must be at least 3 characters.</hlm-field-error>
<hlm-field-error validator="maxlength">Username must be at most 10 characters.</hlm-field-error>
<hlm-field-error validator="pattern">
Username can only contain letters, numbers, and underscores.
</hlm-field-error>
</hlm-field>
</hlm-field-group>
</form>
</div>
<hlm-card-footer>
<hlm-field orientation="horizontal">
<button hlmBtn variant="outline" type="button" (click)="form.reset()">Reset</button>
<button hlmBtn type="submit" form="form-input-demo">Submit</button>
</hlm-field>
</hlm-card-footer>
</hlm-card>
`,
})
export class ReactiveFormInputDemo {
private readonly _fb = inject(FormBuilder);
public form = this._fb.group({
username: [
'',
[Validators.required, Validators.minLength(3), Validators.maxLength(10), Validators.pattern(/^[a-zA-Z0-9_]+$/)],
],
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('You submitted the following values:', JSON.stringify(this.form.value, null, 2));
}
}Textarea
Personalization
Customize your experience by telling us more about yourself.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmFieldImports } from '@spartan-ng/helm/field';
import { HlmTextareaImports } from '@spartan-ng/helm/textarea';
@Component({
selector: 'spartan-reactive-form-textarea-demo',
imports: [ReactiveFormsModule, HlmCardImports, HlmFieldImports, HlmTextareaImports, HlmButtonImports],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'w-full sm:max-w-md' },
template: `
<hlm-card>
<hlm-card-header>
<h3 hlmCardTitle>Personalization</h3>
<p hlmCardDescription>Customize your experience by telling us more about yourself.</p>
</hlm-card-header>
<div hlmCardContent>
<form id="form-textarea-demo" [formGroup]="form" (ngSubmit)="submit()">
<hlm-field-group>
<hlm-field>
<label hlmFieldLabel for="about">More about you</label>
<textarea
hlmTextarea
id="about"
class="min-h-32"
placeholder="I'm a software engineer..."
formControlName="about"
></textarea>
<hlm-field-description>
Tell us more about yourself. This will be used to help us personalize your experience.
</hlm-field-description>
<hlm-field-error validator="required">This is a required field.</hlm-field-error>
<hlm-field-error validator="minlength">Please provide at least 10 characters.</hlm-field-error>
<hlm-field-error validator="maxlength">Please keep it under 200 characters.</hlm-field-error>
</hlm-field>
</hlm-field-group>
</form>
</div>
<hlm-card-footer>
<hlm-field orientation="horizontal">
<button hlmBtn variant="outline" type="button" (click)="form.reset()">Reset</button>
<button hlmBtn type="submit" form="form-textarea-demo">Submit</button>
</hlm-field>
</hlm-card-footer>
</hlm-card>
`,
})
export class ReactiveFormTextareaDemo {
private readonly _fb = inject(FormBuilder);
public form = this._fb.group({
about: ['', [Validators.required, Validators.minLength(10), Validators.maxLength(200)]],
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('You submitted the following values:', JSON.stringify(this.form.value, null, 2));
}
}Select
Language Preferences
Select your preferred spoken language.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
AbstractControl,
FormBuilder,
ReactiveFormsModule,
ValidationErrors,
ValidatorFn,
Validators,
} from '@angular/forms';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmFieldImports } from '@spartan-ng/helm/field';
import { HlmSelectImports } from '@spartan-ng/helm/select';
@Component({
selector: 'spartan-reactive-form-select-demo',
imports: [ReactiveFormsModule, HlmCardImports, HlmFieldImports, HlmSelectImports, HlmButtonImports],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'w-full sm:max-w-md' },
template: `
<hlm-card>
<hlm-card-header>
<h3 hlmCardTitle>Language Preferences</h3>
<p hlmCardDescription>Select your preferred spoken language.</p>
</hlm-card-header>
<div hlmCardContent>
<form id="form-select-demo" [formGroup]="form" (ngSubmit)="submit()">
<hlm-field-group>
<hlm-field orientation="responsive">
<hlm-field-content>
<label hlmFieldLabel for="language">Spoken Language</label>
<hlm-field-description>For best results, select the language you speak.</hlm-field-description>
<hlm-field-error validator="required">Please select your spoken language.</hlm-field-error>
<hlm-field-error validator="autoDetect">
Auto-detection is not allowed. Please select a specific language.
</hlm-field-error>
</hlm-field-content>
<hlm-select formControlName="language" [itemToString]="itemToString">
<hlm-select-trigger buttonId="language">
<hlm-select-value placeholder="Select" />
</hlm-select-trigger>
<hlm-select-content *hlmSelectPortal>
<hlm-select-group>
<hlm-select-item value="auto">Auto</hlm-select-item>
<div hlmSelectSeparator></div>
@for (language of spokenLanguages; track $index) {
<hlm-select-item [value]="language.value">{{ language.label }}</hlm-select-item>
}
</hlm-select-group>
</hlm-select-content>
</hlm-select>
</hlm-field>
</hlm-field-group>
</form>
</div>
<hlm-card-footer>
<hlm-field orientation="horizontal">
<button hlmBtn variant="outline" type="button" (click)="form.reset()">Reset</button>
<button hlmBtn type="submit" form="form-select-demo">Submit</button>
</hlm-field>
</hlm-card-footer>
</hlm-card>
`,
})
export class ReactiveFormSelectDemo {
private readonly _fb = inject(FormBuilder);
public spokenLanguages = [
{ label: 'English', value: 'en' },
{ label: 'Spanish', value: 'es' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
{ label: 'Italian', value: 'it' },
{ label: 'Chinese', value: 'zh' },
{ label: 'Japanese', value: 'ja' },
];
public itemToString = (value: string) => {
if (value === 'auto') {
return 'Auto';
}
const language = this.spokenLanguages.find((lang) => lang.value === value);
return language ? language.label : '';
};
public form = this._fb.group({
language: ['', [Validators.required, autoDetectLanguage()]],
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('You submitted the following values:', JSON.stringify(this.form.value, null, 2));
}
}
function autoDetectLanguage(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return control.value === 'auto' ? { autoDetect: true } : null;
};
}Checkbox
Notifications
Manage your notification preferences.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmCheckboxImports } from '@spartan-ng/helm/checkbox';
import { HlmFieldImports } from '@spartan-ng/helm/field';
@Component({
selector: 'spartan-reactive-form-checkbox-demo',
imports: [ReactiveFormsModule, HlmCardImports, HlmFieldImports, HlmCheckboxImports, HlmButtonImports],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'w-full sm:max-w-md' },
template: `
<hlm-card>
<hlm-card-header>
<h3 hlmCardTitle>Notifications</h3>
<p hlmCardDescription>Manage your notification preferences.</p>
</hlm-card-header>
<div hlmCardContent>
<form id="form-checkbox-demo" [formGroup]="form" (ngSubmit)="submit()">
<hlm-field-group>
<fieldset hlmFieldSet>
<legend hlmFieldLegend variant="label">Responses</legend>
<hlm-field-description>
Get notified for requests that take time, like research or image generation.
</hlm-field-description>
<hlm-field-group>
<hlm-field orientation="horizontal">
<hlm-checkbox inputId="responses" formControlName="responses" />
<label hlmFieldLabel class="font-normal" for="responses">Push notifications</label>
</hlm-field>
</hlm-field-group>
</fieldset>
<hlm-field-separator />
<hlm-field-group>
<fieldset hlmFieldSet>
<legend hlmFieldLegend variant="label">Tasks</legend>
<hlm-field-description>Get notified when tasks you've created have updates.</hlm-field-description>
<hlm-field-group data-slot="checkbox-group">
@for (task of tasks; track task.id) {
<hlm-field orientation="horizontal">
<hlm-checkbox
[inputId]="'task-' + task.id"
[checked]="form.controls.tasks.value.includes(task.id)"
(checkedChange)="handleChange($event, task.id)"
/>
<label hlmFieldLabel class="font-normal" [for]="'task-' + task.id">{{ task.label }}</label>
</hlm-field>
}
</hlm-field-group>
</fieldset>
@if (form.controls.tasks.invalid && form.controls.tasks.touched) {
<hlm-field-error>Please select at least one notification type.</hlm-field-error>
}
</hlm-field-group>
</hlm-field-group>
</form>
</div>
<hlm-card-footer>
<hlm-field orientation="horizontal">
<button hlmBtn variant="outline" type="button" (click)="form.reset({ responses: true, tasks: [] })">
Reset
</button>
<button hlmBtn type="submit" form="form-checkbox-demo">Submit</button>
</hlm-field>
</hlm-card-footer>
</hlm-card>
`,
})
export class ReactiveFormCheckboxDemo {
private readonly _fb = inject(FormBuilder);
public tasks = [
{
id: 'push',
label: 'Push notifications',
},
{
id: 'email',
label: 'Email notifications',
},
];
public form = this._fb.group({
responses: [{ value: true, disabled: true }],
tasks: this._fb.array([], Validators.required),
});
handleChange(checked: boolean, id: string) {
const tasks = this.form.controls.tasks;
if (checked) {
tasks.push(this._fb.control(id));
} else {
const index = tasks.controls.findIndex((x) => x.value === id);
tasks.removeAt(index);
}
this.form.controls.tasks.markAsTouched();
}
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('You submitted the following values:', JSON.stringify(this.form.getRawValue(), null, 2));
}
}Radio Group
Subscription Plan
See pricing and features for each plan.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmFieldImports } from '@spartan-ng/helm/field';
import { HlmRadioGroupImports } from '@spartan-ng/helm/radio-group';
@Component({
selector: 'spartan-reactive-form-radio-group-demo',
imports: [ReactiveFormsModule, HlmCardImports, HlmFieldImports, HlmRadioGroupImports, HlmButtonImports],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'w-full sm:max-w-md' },
template: `
<hlm-card>
<hlm-card-header>
<h3 hlmCardTitle>Subscription Plan</h3>
<p hlmCardDescription>See pricing and features for each plan.</p>
</hlm-card-header>
<div hlmCardContent>
<form id="form-radio-group-demo" [formGroup]="form" (ngSubmit)="submit()">
<hlm-field-group>
<fieldset hlmFieldSet>
<legend hlmFieldLegend>Plan</legend>
<hlm-field-description>You can upgrade or downgrade your plan at any time.</hlm-field-description>
<hlm-radio-group formControlName="plan">
@for (plan of plans; track plan.id) {
<label hlmFieldLabel [for]="'plan-' + plan.id">
<hlm-field orientation="horizontal">
<hlm-field-content>
<hlm-field-title>{{ plan.title }}</hlm-field-title>
<hlm-field-description>{{ plan.description }}</hlm-field-description>
</hlm-field-content>
<hlm-radio [value]="plan.id" [inputId]="'plan-' + plan.id">
<hlm-radio-indicator indicator />
</hlm-radio>
</hlm-field>
</label>
}
</hlm-radio-group>
</fieldset>
@if (form.controls.plan.invalid && form.controls.plan.touched) {
<hlm-field-error>You must select a subscription plan to continue.</hlm-field-error>
}
</hlm-field-group>
</form>
</div>
<hlm-card-footer>
<hlm-field orientation="horizontal">
<button hlmBtn variant="outline" type="button" (click)="form.reset()">Reset</button>
<button hlmBtn type="submit" form="form-radio-group-demo">Submit</button>
</hlm-field>
</hlm-card-footer>
</hlm-card>
`,
})
export class ReactiveFormRadioGroupDemo {
private readonly _fb = inject(FormBuilder);
public plans = [
{
id: 'starter',
title: 'Starter (100K tokens/month)',
description: 'For everyday use with basic features.',
},
{
id: 'pro',
title: 'Pro (1M tokens/month)',
description: 'For advanced AI usage with more features.',
},
{
id: 'enterprise',
title: 'Enterprise (Unlimited tokens)',
description: 'For large teams and heavy usage.',
},
];
public form = this._fb.group({
plan: ['', Validators.required],
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('You submitted the following values:', JSON.stringify(this.form.value, null, 2));
}
}Switch
Security Settings
Manage your account security preferences.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCardImports } from '@spartan-ng/helm/card';
import { HlmFieldImports } from '@spartan-ng/helm/field';
import { HlmSwitchImports } from '@spartan-ng/helm/switch';
@Component({
selector: 'spartan-reactive-form-switch-demo',
imports: [ReactiveFormsModule, HlmCardImports, HlmFieldImports, HlmSwitchImports, HlmButtonImports],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'w-full sm:max-w-md' },
template: `
<hlm-card>
<hlm-card-header>
<h3 hlmCardTitle>Security Settings</h3>
<p hlmCardDescription>Manage your account security preferences.</p>
</hlm-card-header>
<div hlmCardContent>
<form id="form-switch-demo" [formGroup]="form" (ngSubmit)="submit()">
<hlm-field-group>
<hlm-field orientation="horizontal">
<hlm-field-content>
<label hlmFieldLabel for="two-factor">Multi-factor authentication</label>
<hlm-field-description>
Enable multi-factor authentication to secure your account.
</hlm-field-description>
<hlm-field-error>It is highly recommended to enable two-factor authentication.</hlm-field-error>
</hlm-field-content>
<hlm-switch inputId="two-factor" formControlName="twoFactor" />
</hlm-field>
</hlm-field-group>
</form>
</div>
<hlm-card-footer>
<hlm-field orientation="horizontal">
<button hlmBtn variant="outline" type="button" (click)="form.reset({ twoFactor: false })">Reset</button>
<button hlmBtn type="submit" form="form-switch-demo">Submit</button>
</hlm-field>
</hlm-card-footer>
</hlm-card>
`,
})
export class ReactiveFormSwitchDemo {
private readonly _fb = inject(FormBuilder);
public form = this._fb.group({
twoFactor: [false, Validators.requiredTrue],
});
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('You submitted the following values:', JSON.stringify(this.form.value, null, 2));
}
}Resetting the form
Use form.reset() to reset the form to its default values and mark all controls as pristine and untouched.
<button hlmBtn variant="outline" type="button" (click)="form.reset()">
Reset
</button>On This Page