Input
Displays a form input field.
- v1.0.0
- SSR
- Zoneless
- 1.8 kB gzip
import { Component } from '@angular/core';
import { Input } from '@/ui/input/input';
/** A plain email input. */
@Component({
selector: 'example-input-demo',
imports: [Input],
template: `
<input
ndInput
type="email"
placeholder="Email"
aria-label="Email"
class="max-w-sm"
/>
`,
})
export class InputDemoExample {}
A text input collects a single line of text. ndInput styles a native
input and adds no value accessor, so forms bind it directly: Signal Forms
with [formField], reactive forms with formControlName, or plain
attributes. It reads the bound field's state and shows the input as invalid
once the field is invalid and touched, with aria-invalid and
data-invalid.
Installation
npx @ndop/cli add inputpnpm dlx @ndop/cli add inputyarn dlx @ndop/cli add inputbunx @ndop/cli add inputInstall the dependencies
npm install @ndop/primitives @ndop/corepnpm add @ndop/primitives @ndop/coreyarn add @ndop/primitives @ndop/corebun add @ndop/primitives @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/input/input.tsimport { computed, Directive, input } from '@angular/core'; import { NdInput } from '@ndop/primitives/input'; import { cn } from '@/lib/utils'; /** * Styled text input. Forms bind the native input directly; the * `@ndop/primitives/input` behavior reflects their state as * `aria-invalid` once the field is touched. * * @example * ```html * <input ndInput id="email" type="email" [formField]="form.email" /> * ``` */ @Directive({ selector: 'input[ndInput]', hostDirectives: [{ directive: NdInput, inputs: ['id', 'invalid'] }], host: { 'data-slot': 'input', '[class]': 'classes()' }, }) export class Input { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30', 'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50', 'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40', this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import { Input } from '@/ui/input/input';<input
ndInput
type="email"
placeholder="Email"
aria-label="Email"
class="max-w-sm"
/>Examples
Input signal forms
An input bound with Signal Forms. It turns invalid once the field is touched and fails validation, and the label follows it.
We'll send the invite here.
import { Component, signal } from '@angular/core';
import { email, form, FormField, required } from '@angular/forms/signals';
import { Input } from '@/ui/input/input';
import { Label } from '@/ui/label/label';
/**
* An input bound with Signal Forms. It turns invalid once the field is
* touched and fails validation, and the label follows it.
*/
@Component({
selector: 'example-input-signal-forms',
imports: [Input, Label, FormField],
template: `
<div class="grid w-full max-w-sm gap-3">
<label ndLabel for="input-signal-forms-email">Work email</label>
<input
ndInput
id="input-signal-forms-email"
type="email"
placeholder="[email protected]"
aria-describedby="input-signal-forms-hint"
[formField]="account.email"
/>
<p id="input-signal-forms-hint" class="text-muted-foreground text-sm">
@if (account.email().touched() && account.email().invalid()) {
Enter a valid email address.
} @else {
We'll send the invite here.
}
</p>
</div>
`,
})
export class InputSignalFormsExample {
private readonly model = signal({ email: '' });
protected readonly account = form(this.model, (path) => {
required(path.email);
email(path.email);
});
}
Input with button
An input and a button on one row, as in a newsletter sign-up.
import { Component } from '@angular/core';
import { Button } from '@/ui/button/button';
import { Input } from '@/ui/input/input';
/** An input and a button on one row, as in a newsletter sign-up. */
@Component({
selector: 'example-input-with-button',
imports: [Input, Button],
template: `
<form
class="flex w-full max-w-sm items-center gap-2"
(submit)="$event.preventDefault()"
>
<input ndInput type="email" placeholder="Email" aria-label="Email" />
<button ndButton type="submit" variant="outline">Subscribe</button>
</form>
`,
})
export class InputWithButtonExample {}
Input disabled
A disabled input with its label.
import { Component } from '@angular/core';
import { Input } from '@/ui/input/input';
import { Label } from '@/ui/label/label';
/** A disabled input; its label dims with it. */
@Component({
selector: 'example-input-disabled',
imports: [Input, Label],
template: `
<div class="grid w-full max-w-sm gap-3">
<label ndLabel for="input-disabled-email">Email</label>
<input
ndInput
id="input-disabled-email"
type="email"
value="[email protected]"
disabled
/>
</div>
`,
})
export class InputDisabledExample {}
Accessibility
Focus
- Unchanged from the native input. Focusing it and leaving it marks the bound field touched, through the forms library.
Right-to-left
Text direction follows the document or the element's dir; Ndop changes nothing.
API reference
Input
input[ndInput]role nativeStyled text input. Forms bind the native input directly; the
@ndop/primitives/input behavior reflects their state as
aria-invalid once the field is touched.
Manages aria-invalid, aria-describedby.
| Input binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
[id]Element id; also the key a label[ndLabel] uses to find this input. | string | undefined | — |
[invalid]Marks the input invalid when no forms library is bound; with one, the
field's state decides. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | input |
data-disabled | — |
data-invalid | — |
Agent notes
Do
- Pair every input with an
ndLabelwhoseformatches the input'sid. - Bind it with Signal Forms (
[formField]) or reactive forms, and let the field's validity drive the invalid state. - Set the right
type(email,password,search,tel) so devices show the matching keyboard and browsers can autofill.
Don't
- Don't set
invalidwhile a forms library is bound; the field's state decides. - Don't rely on color alone for errors; describe them in text linked with
aria-describedby.