npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ng-vui/text-input

v1.0.0

Published

Angular Text Input Component for NG-VUI Library

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/forms

Required 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:

  1. Check Tailwind CSS - The component uses Tailwind utility classes
  2. Verify CSS Import - Ensure the CSS file is properly imported
  3. Check Build Process - Make sure CSS files are included in your build
  4. 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 for attribute 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

  1. Use OnPush Change Detection
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  // ...
})
  1. Debounce Input Events for search functionality
  2. Use TrackBy functions when rendering multiple inputs in lists
  3. 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 sections
  • ng-vui-multi-select - For dropdown selections
  • ng-vui-date-picker - For date inputs
  • ng-vui-modal - For form dialogs
  • ng-vui-file-uploader-mini - For file upload forms
  • ng-vui-switch - For toggle inputs
  • ng-vui-rating-stars - For rating inputs

🤝 Contributing

We welcome contributions! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Submit a pull request

📄 License

MIT © VUI


Need help? Check our documentation or open an issue on GitHub.