@ng-vui/text-input
v1.0.0
Published
Angular Text Input Component for NG-VUI Library
Maintainers
Readme
NgVuiTextInput Component
A fully-featured, accessible Angular text input component with built-in validation, error handling, and seamless form integration. Part of the ng-vui component library, designed for production-ready applications with comprehensive error messaging and visual feedback.
🚀 Features
- ✅ Reactive Forms Integration - Works seamlessly with FormControl and FormGroup
- ✅ Built-in Validation - Supports all Angular validators with automatic error messages
- ✅ Error Handling - Visual error states with customizable error messages and icons
- ✅ Accessibility - Full ARIA support, keyboard navigation, and screen reader compatibility
- ✅ TypeScript - Full type safety and IntelliSense support
- ✅ Control Value Accessor - Standard Angular form control interface
- ✅ Event Handling - Input, blur, and change event emitters
- ✅ Auto-generated IDs - Automatic element ID generation for accessibility
- ✅ Required Indicators - Visual asterisk for required fields
- ✅ Customizable Styling - Tailwind CSS with error state styling
📦 Installation
The component is part of the ng-vui component library:
import { NgVuiTextInputComponent } from 'path/to/ng-vui-text-input';Dependencies
npm install @angular/core @angular/common @angular/formsRequired CSS Framework
This component is styled with Tailwind CSS classes. Ensure your project has Tailwind CSS configured.
CSS Setup
The component uses a combination of Tailwind CSS utility classes and custom CSS. Ensure your Tailwind CSS configuration includes all necessary utilities.
Custom CSS Classes
The component includes custom CSS classes for enhanced styling:
.animate-fadeIn- Smooth fade-in animation for error messages- Custom focus ring utilities
- Error state styling overrides
Including Component Styles
If using as a standalone component, include the component's CSS file in your build process or copy the styles to your global stylesheet.
Visual Features You'll Get with Proper CSS:
- ✨ Rounded input fields with smooth borders
- 🎯 Violet focus rings for accessibility
- 🚨 Red error states with background tint and borders
- 🔴 Error icons with animated messages
- ⭐ Required field indicators with red asterisks
- 🎨 Smooth transitions for all state changes
- 📱 Responsive design that works on all screen sizes
Troubleshooting Styling Issues:
If you're still not seeing proper styling:
- Check Tailwind CSS - The component uses Tailwind utility classes
- Verify CSS Import - Ensure the CSS file is properly imported
- Check Build Process - Make sure CSS files are included in your build
- Browser DevTools - Inspect elements to see if CSS classes are applied
🎯 Basic Usage
Simple Text Input
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgVuiTextInputComponent } from 'path/to/ng-vui-text-input';
@Component({
standalone: true,
imports: [FormsModule, NgVuiTextInputComponent],
template: `
<ng-vui-text-input
label="Full Name"
placeholder="Enter your full name"
[(ngModel)]="name">
</ng-vui-text-input>
`
})
export class MyComponent {
name = '';
}With Reactive Forms and Validation
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { NgVuiTextInputComponent } from 'path/to/ng-vui-text-input';
@Component({
standalone: true,
imports: [ReactiveFormsModule, NgVuiTextInputComponent],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<ng-vui-text-input
label="Email Address"
type="email"
placeholder="Enter your email"
formControlName="email"
requiredError="Email is required for account creation"
[submitted]="isSubmitted">
</ng-vui-text-input>
<ng-vui-text-input
label="Password"
type="password"
placeholder="Enter your password"
formControlName="password"
requiredError="Password is required"
[submitted]="isSubmitted">
</ng-vui-text-input>
<button type="submit" class="mt-4 px-4 py-2 bg-blue-600 text-white rounded">
Create Account
</button>
</form>
`
})
export class UserFormComponent implements OnInit {
userForm!: FormGroup;
isSubmitted = false;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.userForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
});
}
onSubmit() {
this.isSubmitted = true;
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
} else {
console.log('Form has errors');
}
}
}🔧 API Reference
Component Inputs
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| label | string | '' | Label text displayed above the input |
| type | string | 'text' | HTML input type (text, email, password, number, etc.) |
| id | string \| undefined | undefined | Custom element ID (auto-generated if not provided) |
| placeholder | string | '' | Placeholder text for the input field |
| requiredError | string | 'This field is required' | Custom error message for required validation |
| customError | string \| null | null | Custom error message to override all validation errors |
| submitted | boolean | false | Whether the parent form has been submitted (triggers error display) |
Supported Input Types
The component supports all standard HTML input types:
| Type | Description | Example Use Case |
|------|-------------|------------------|
| text | Default text input | Names, titles, general text |
| email | Email validation | Email addresses with built-in validation |
| password | Masked text input | Passwords, secure fields |
| number | Numeric input | Ages, quantities, prices |
| tel | Telephone number | Phone numbers |
| url | URL validation | Website addresses |
| search | Search input | Search boxes |
| date | Date picker | Date selection |
| time | Time picker | Time selection |
| datetime-local | Date and time | Combined date/time selection |
Component Outputs
| Event | Type | Description |
|-------|------|-------------|
| blur | EventEmitter<string> | Emitted when input loses focus, returns current value |
| change | EventEmitter<string> | Emitted when input value changes and loses focus |
| input | EventEmitter<string> | Emitted on every keystroke/input event |
Form Integration
The component implements Angular's ControlValueAccessor interface and supports:
- FormControl -
new FormControl('initial value') - FormGroup -
formControlName="fieldName" - NgModel -
[(ngModel)]="property" - Template-driven forms - Works with template reference variables
- Reactive forms - Full validation and state management
🚨 Error Handling & Validation
Built-in Validator Support
The component automatically handles and displays error messages for common Angular validators:
Required Validation
@Component({
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<ng-vui-text-input
label="Full Name"
formControlName="name"
requiredError="Full name is required for registration"
[submitted]="submitted">
</ng-vui-text-input>
</form>
`
})
export class RequiredValidationExample {
form = this.fb.group({
name: ['', Validators.required] // Shows custom error message
});
submitted = false;
onSubmit() {
this.submitted = true; // Triggers error display
}
}Email Validation
// Automatically shows: "Please enter a valid email address"
this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]]
});Length Validation
@Component({
template: `
<ng-vui-text-input
label="Username"
formControlName="username"
placeholder="3-20 characters"
[submitted]="submitted">
</ng-vui-text-input>
<!-- Shows: "Minimum length is 3 characters" -->
<!-- Shows: "Maximum length is 20 characters" -->
`
})
export class LengthValidationExample {
form = this.fb.group({
username: ['', [
Validators.required,
Validators.minLength(3), // Shows: "Minimum length is 3 characters"
Validators.maxLength(20) // Shows: "Maximum length is 20 characters"
]]
});
}Pattern Validation
@Component({
template: `
<ng-vui-text-input
label="Phone Number"
formControlName="phone"
placeholder="(555) 123-4567"
[submitted]="submitted">
</ng-vui-text-input>
<!-- Shows: "Please enter a valid format" -->
`
})
export class PatternValidationExample {
form = this.fb.group({
phone: ['', [
Validators.required,
Validators.pattern(/^\(\d{3}\) \d{3}-\d{4}$/) // Shows: "Please enter a valid format"
]]
});
}Custom Error Messages
Override Default Messages
@Component({
template: `
<ng-vui-text-input
label="Credit Card"
formControlName="creditCard"
[customError]="getCreditCardError()"
[submitted]="submitted">
</ng-vui-text-input>
`
})
export class CustomErrorExample {
form = this.fb.group({
creditCard: ['', [Validators.required, this.creditCardValidator]]
});
getCreditCardError(): string | null {
const control = this.form.get('creditCard');
if (control?.errors?.['required'] && (control.touched || this.submitted)) {
return 'Credit card number is required for payment';
}
if (control?.errors?.['invalidCard'] && (control.touched || this.submitted)) {
return 'Please enter a valid credit card number (16 digits)';
}
return null; // No custom error, use default
}
creditCardValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (value && !/^\d{16}$/.test(value.replace(/\s/g, ''))) {
return { invalidCard: true };
}
return null;
}
}🎯 Advanced Examples
Complex Form with Multiple Validations
@Component({
template: `
<form [formGroup]="registrationForm" (ngSubmit)="onRegister()">
<div class="space-y-4">
<!-- Email with custom error -->
<ng-vui-text-input
label="Email Address"
type="email"
formControlName="email"
placeholder="[email protected]"
requiredError="Email is required to create your account"
[submitted]="isSubmitted">
</ng-vui-text-input>
<!-- Password with length validation -->
<ng-vui-text-input
label="Password"
type="password"
formControlName="password"
placeholder="At least 8 characters"
requiredError="Password is required for account security"
[submitted]="isSubmitted">
</ng-vui-text-input>
<!-- Confirm password with custom validation -->
<ng-vui-text-input
label="Confirm Password"
type="password"
formControlName="confirmPassword"
placeholder="Re-enter your password"
[customError]="getPasswordMatchError()"
[submitted]="isSubmitted">
</ng-vui-text-input>
<!-- Username with pattern validation -->
<ng-vui-text-input
label="Username"
formControlName="username"
placeholder="letters, numbers, underscore only"
[submitted]="isSubmitted">
</ng-vui-text-input>
<button type="submit"
class="w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Create Account
</button>
</div>
</form>
`
})
export class RegistrationFormComponent {
registrationForm: FormGroup;
isSubmitted = false;
constructor(private fb: FormBuilder) {
this.registrationForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [
Validators.required,
Validators.minLength(8),
this.passwordStrengthValidator
]],
confirmPassword: ['', Validators.required],
username: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(20),
Validators.pattern(/^[a-zA-Z0-9_]+$/)
]]
}, {
validators: this.passwordMatchValidator
});
}
onRegister() {
this.isSubmitted = true;
if (this.registrationForm.valid) {
console.log('Registration successful!', this.registrationForm.value);
} else {
console.log('Please fix form errors');
}
}
getPasswordMatchError(): string | null {
const form = this.registrationForm;
const password = form?.get('password')?.value;
const confirmPassword = form?.get('confirmPassword')?.value;
if (form?.errors?.['passwordMismatch'] &&
(form.get('confirmPassword')?.touched || this.isSubmitted)) {
return 'Passwords do not match';
}
return null;
}
passwordMatchValidator(form: AbstractControl): ValidationErrors | null {
const password = form.get('password')?.value;
const confirmPassword = form.get('confirmPassword')?.value;
if (password !== confirmPassword) {
return { passwordMismatch: true };
}
return null;
}
passwordStrengthValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumber = /[0-9]/.test(value);
if (hasUpperCase && hasLowerCase && hasNumber) {
return null;
}
return { passwordWeak: { message: 'Password must contain uppercase, lowercase, and number' } };
}
}Real-time Search with Debouncing
@Component({
template: `
<div>
<ng-vui-text-input
label="Search Users"
placeholder="Type to search users..."
formControlName="search"
(input)="onSearchInput($event)">
</ng-vui-text-input>
<div *ngIf="searchResults.length > 0" class="mt-2 border rounded-lg">
<div *ngFor="let result of searchResults"
class="p-3 border-b last:border-b-0 hover:bg-gray-50">
<div class="font-medium">{{ result.name }}</div>
<div class="text-sm text-gray-600">{{ result.email }}</div>
</div>
</div>
<div *ngIf="isSearching" class="mt-2 text-gray-600">
Searching...
</div>
</div>
`
})
export class SearchComponent implements OnInit, OnDestroy {
searchForm = this.fb.group({
search: ['']
});
searchResults: any[] = [];
isSearching = false;
private searchSubject = new Subject<string>();
private destroy$ = new Subject<void>();
ngOnInit() {
// Debounce search input
this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
takeUntil(this.destroy$)
).subscribe(term => {
this.performSearch(term);
});
}
onSearchInput(searchTerm: string) {
this.searchSubject.next(searchTerm);
}
performSearch(term: string) {
if (term.length < 2) {
this.searchResults = [];
return;
}
this.isSearching = true;
// Simulate API call
setTimeout(() => {
this.searchResults = [
{ name: `User matching "${term}"`, email: '[email protected]' },
{ name: `Another user with "${term}"`, email: '[email protected]' }
];
this.isSearching = false;
}, 500);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}Event Handling Example
@Component({
template: `
<ng-vui-text-input
label="Comment"
placeholder="Write a comment..."
formControlName="comment"
(input)="onCommentInput($event)"
(blur)="onCommentBlur($event)"
(change)="onCommentChange($event)">
</ng-vui-text-input>
<div class="mt-2 text-sm text-gray-600">
<div>Characters: {{ characterCount }}/500</div>
<div>Status: {{ inputStatus }}</div>
<div>Last changed: {{ lastChanged | date:'medium' }}</div>
</div>
`
})
export class EventHandlingExample {
form = this.fb.group({
comment: ['']
});
characterCount = 0;
inputStatus = 'Ready';
lastChanged: Date | null = null;
onCommentInput(value: string) {
this.characterCount = value.length;
this.inputStatus = 'Typing...';
}
onCommentBlur(value: string) {
this.inputStatus = 'Focus lost';
console.log('Comment blurred with value:', value);
}
onCommentChange(value: string) {
this.lastChanged = new Date();
this.inputStatus = 'Changed';
console.log('Comment changed to:', value);
}
}🎨 Visual States & Styling
Default State
- Clean rounded input with gray border
- Violet focus ring and border
- Smooth transitions and hover effects
- Gray placeholder text
Error State
- Red border and background tint
- Red focus ring and border
- Error icon with descriptive message
- Red label and error text
Required Field Indicator
- Red asterisk (*) next to label
- Only shown when field has required validator
Disabled State (when using FormControl)
// Disabled through form control
this.form.get('fieldName')?.disable();- Gray background
- Disabled cursor
- Muted text color
🌐 Accessibility Features
- Proper Labels - Automatic
forattribute linking label to input - Required Indicators - Visual asterisk for required fields
- Error Announcements - Screen readers announce validation errors
- Focus Management - Clear focus indicators with violet ring
- Keyboard Navigation - Full tab and keyboard support
- Auto-generated IDs - Unique element IDs for accessibility
🧪 Testing Examples
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { NgVuiTextInputComponent } from 'path/to/ng-vui-text-input';
import { Component } from '@angular/core';
@Component({
template: `
<form [formGroup]="testForm">
<ng-vui-text-input
label="Test Input"
formControlName="testField"
requiredError="Test field is required"
[submitted]="submitted">
</ng-vui-text-input>
</form>
`
})
class TestHostComponent {
testForm = this.fb.group({
testField: ['', Validators.required]
});
submitted = false;
constructor(private fb: FormBuilder) {}
}
describe('TextInputComponent Integration', () => {
let component: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ReactiveFormsModule, NgVuiTextInputComponent],
declarations: [TestHostComponent]
}).compileComponents();
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should show required error when submitted without value', () => {
component.submitted = true;
fixture.detectChanges();
const errorElement = fixture.nativeElement.querySelector('.text-red-600');
expect(errorElement).toBeTruthy();
expect(errorElement.textContent).toContain('Test field is required');
});
it('should emit input events correctly', () => {
const inputElement = fixture.nativeElement.querySelector('input');
const textInputComponent = fixture.debugElement.query(
By.directive(NgVuiTextInputComponent)
).componentInstance;
spyOn(textInputComponent.input, 'emit');
inputElement.value = 'test value';
inputElement.dispatchEvent(new Event('input'));
expect(textInputComponent.input.emit).toHaveBeenCalledWith('test value');
});
it('should validate email format', () => {
component.testForm.get('testField')?.setValidators([Validators.email]);
component.testForm.get('testField')?.setValue('invalid-email');
component.testForm.get('testField')?.markAsTouched();
fixture.detectChanges();
const errorElement = fixture.nativeElement.querySelector('.text-red-600');
expect(errorElement.textContent).toContain('Please enter a valid email address');
});
});💡 Best Practices
✅ Do
// Use meaningful labels and placeholders
<ng-vui-text-input
label="Email Address"
placeholder="[email protected]"
type="email"
formControlName="email">
</ng-vui-text-input>
// Handle form submission state properly
onSubmit() {
this.isSubmitted = true;
if (this.form.valid) {
// Process form
}
}
// Use appropriate input types
<ng-vui-text-input type="email" ...> <!-- for emails -->
<ng-vui-text-input type="password" ...> <!-- for passwords -->
<ng-vui-text-input type="tel" ...> <!-- for phone numbers -->
// Provide custom error messages for better UX
<ng-vui-text-input
requiredError="Email is required to create your account"
formControlName="email">
</ng-vui-text-input>❌ Don't
// Don't use generic labels
<ng-vui-text-input label="Input" placeholder="Enter value">
// Don't forget to handle submitted state
<ng-vui-text-input formControlName="email"> <!-- Missing [submitted] binding -->
// Don't ignore validation setup
this.form = this.fb.group({
email: [''] // Missing validators
});
// Don't use without proper form integration
<input type="text" /> <!-- Use the component instead -->🚀 Performance Tips
- Use OnPush Change Detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})- Debounce Input Events for search functionality
- Use TrackBy functions when rendering multiple inputs in lists
- Lazy Load Validation for complex forms with conditional fields
🔗 Related Components
This text input works seamlessly with other ng-vui library components:
ng-vui-accordion- For collapsible form sectionsng-vui-multi-select- For dropdown selectionsng-vui-date-picker- For date inputsng-vui-modal- For form dialogsng-vui-file-uploader-mini- For file upload formsng-vui-switch- For toggle inputsng-vui-rating-stars- For rating inputs
🤝 Contributing
We welcome contributions! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Submit a pull request
📄 License
MIT © VUI
Need help? Check our documentation or open an issue on GitHub.
