/touchspin-angular

Primary LanguageTypeScriptOtherNOASSERTION

TouchSpin Angular

Angular adapter for TouchSpin numeric input spinner - Native Angular components with full framework integration.

Features

  • Native Angular components with ControlValueAccessor
  • Reactive and template-driven forms support
  • Per-renderer subpath imports (Bootstrap 3/4/5, Tailwind, Vanilla)
  • Standalone components (Angular 14+)
  • SSR/Angular Universal compatible
  • Full keyboard navigation and ARIA attributes
  • Comprehensive test coverage
  • Complete TouchSpin API support

Installation

npm install @touchspin/angular @touchspin/core @touchspin/renderer-bootstrap5
# or
yarn add @touchspin/angular @touchspin/core @touchspin/renderer-bootstrap5
# or
pnpm add @touchspin/angular @touchspin/core @touchspin/renderer-bootstrap5

Quick Start

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TouchSpinVanillaComponent } from '@touchspin/angular/vanilla';
import '@touchspin/renderer-vanilla/css';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormsModule, TouchSpinVanillaComponent],
  template: `
    <touch-spin
      [(ngModel)]="value"
      [min]="0"
      [max]="100"
      [step]="1"
    ></touch-spin>
    <p>Value: {{ value }}</p>
  `
})
export class AppComponent {
  value = 25;
}

Available Renderers

Choose the renderer that matches your design system:

Renderer Import CSS Import Description
Vanilla @touchspin/angular/vanilla @touchspin/renderer-vanilla/css Clean, framework-free styling
Bootstrap 5 @touchspin/angular/bootstrap5 @touchspin/renderer-bootstrap5/css Bootstrap 5 compatible
Bootstrap 4 @touchspin/angular/bootstrap4 @touchspin/renderer-bootstrap4/css Bootstrap 4 compatible
Bootstrap 3 @touchspin/angular/bootstrap3 @touchspin/renderer-bootstrap3/css Bootstrap 3 compatible
Tailwind @touchspin/angular/tailwind @touchspin/renderer-tailwind/css Tailwind CSS styling

API Reference

Inputs (Properties)

Value Management

<TouchSpin
  [(ngModel)]="controlledValue"        // Template-driven forms
  [value]="controlledValue"            // Reactive forms
  [defaultValue]="initialValue"        // Uncontrolled mode
  (valueChange)="onValueChange($event)" // Value change event
/>

Configuration

<TouchSpin
  [min]="number"                       // Minimum value
  [max]="number"                       // Maximum value
  [step]="number"                      // Increment/decrement step
  [decimals]="number"                  // Decimal places
  [prefix]="'string'"                  // Text before input
  [suffix]="'string'"                  // Text after input
/>

State & Behavior

<TouchSpin
  [disabled]="boolean"                 // Disable input and buttons
  [readOnly]="boolean"                 // Make input read-only
/>

Form Integration

<TouchSpin
  [name]="'fieldName'"                 // Form field name
  [id]="'fieldId'"                     // Input element ID
/>

Styling

<TouchSpin
  [class]="'custom-class'"             // Wrapper CSS class
  [inputClass]="'input-class'"         // Input CSS class
/>

Events

<TouchSpin
  (blur)="onBlur()"                   // Input blur event
  (focus)="onFocus()"                 // Input focus event

  // TouchSpin Events
  (onMin)="onMin()"                   // Fired at minimum boundary
  (onMax)="onMax()"                   // Fired at maximum boundary
  (onStartSpin)="onStartSpin()"       // Fired when spinning starts
  (onStopSpin)="onStopSpin()"         // Fired when spinning stops
  (onStartUpSpin)="onStartUpSpin()"   // Fired when upward spinning starts
  (onStartDownSpin)="onStartDownSpin()" // Fired when downward spinning starts
  (onStopUpSpin)="onStopUpSpin()"     // Fired when upward spinning stops
  (onStopDownSpin)="onStopDownSpin()" // Fired when downward spinning stops
  (onSpeedChange)="onSpeedChange()"   // Fired when spin speed increases
/>

Imperative API (ViewChild)

import { Component, ViewChild } from '@angular/core';
import { TouchSpinVanillaComponent } from '@touchspin/angular/vanilla';

@Component({
  selector: 'app-example',
  template: `
    <touch-spin #touchSpinRef [defaultValue]="50"></touch-spin>
    <button (click)="increment()">+1</button>
  `
})
export class ExampleComponent {
  @ViewChild('touchSpinRef') touchSpin!: TouchSpinVanillaComponent;

  increment() {
    this.touchSpin.increment();
  }
}

TouchSpinHandle Methods

interface TouchSpinHandle {
  // Focus Management
  focus(): void;                    // Focus the input
  blur(): void;                     // Blur the input

  // Value Control
  increment(): void;                // Increment by step
  decrement(): void;                // Decrement by step
  getValue(): number;               // Get current value
  setValue(value: number): void;    // Set new value

  // Continuous Spinning
  startUpSpin(): void;              // Start continuous upward spinning
  startDownSpin(): void;            // Start continuous downward spinning
  stopSpin(): void;                 // Stop any continuous spinning

  // Configuration
  updateSettings(opts: Partial<TouchSpinCoreOptions>): void;
                                  // Update settings at runtime
}

Usage Examples

Basic Controlled Component

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TouchSpinVanillaComponent } from '@touchspin/angular/vanilla';

@Component({
  selector: 'app-basic',
  standalone: true,
  imports: [FormsModule, TouchSpinVanillaComponent],
  template: `
    <div>
      <touch-spin
        [(ngModel)]="value"
        [min]="0"
        [max]="100"
        [step]="5"
      ></touch-spin>
      <p>Value: {{ value }}</p>
    </div>
  `
})
export class BasicComponent {
  value = 25;
}

With Prefix/Suffix

import { Component } from '@angular/core';
import { TouchSpinVanillaComponent } from '@touchspin/angular/vanilla';

@Component({
  selector: 'app-currency',
  standalone: true,
  imports: [TouchSpinVanillaComponent],
  template: `
    <touch-spin
      [(ngModel)]="price"
      [min]="0"
      [max]="1000"
      [step]="0.01"
      [decimals]="2"
      [prefix]="'$'"
      [suffix]="' USD'"
    ></touch-spin>
  `
})
export class CurrencyComponent {
  price = 29.99;
}

Event Handling

import { Component } from '@angular/core';
import { TouchSpinVanillaComponent } from '@touchspin/angular/vanilla';

@Component({
  selector: 'app-events',
  standalone: true,
  imports: [TouchSpinVanillaComponent],
  template: `
    <div>
      <touch-spin
        [defaultValue]="50"
        [min]="0"
        [max]="100"
        (onMin)="addEvent('Reached minimum')"
        (onMax)="addEvent('Reached maximum')"
        (onStartSpin)="addEvent('Spin started')"
        (onStopSpin)="addEvent('Spin stopped')"
      ></touch-spin>

      <h3>Event Log:</h3>
      <ul>
        <li *ngFor="let event of events; trackBy: trackByIndex">{{ event }}</li>
      </ul>
    </div>
  `
})
export class EventsComponent {
  events: string[] = [];

  addEvent(message: string) {
    this.events.unshift(`${new Date().toLocaleTimeString()}: ${message}`);
  }

  trackByIndex(index: number) {
    return index;
  }
}

Imperative Control

import { Component, ViewChild } from '@angular/core';
import { TouchSpinVanillaComponent } from '@touchspin/angular/vanilla';

@Component({
  selector: 'app-imperative',
  standalone: true,
  imports: [TouchSpinVanillaComponent],
  template: `
    <div>
      <div>
        <button (click)="setValue42()">Set to 42</button>
        <button (click)="startSpinning()">Start Spinning Up</button>
        <button (click)="stopSpinning()">Stop Spinning</button>
        <button (click)="showValue()">Show Current Value</button>
      </div>

      <touch-spin
        #touchSpin
        [defaultValue]="25"
        [min]="0"
        [max]="100"
      ></touch-spin>

      <p>Current value: {{ currentValue }}</p>
    </div>
  `
})
export class ImperativeComponent {
  @ViewChild('touchSpin') touchSpin!: TouchSpinVanillaComponent;
  currentValue = 0;

  setValue42() {
    this.touchSpin.setValue(42);
  }

  startSpinning() {
    this.touchSpin.startUpSpin();
  }

  stopSpinning() {
    this.touchSpin.stopSpin();
  }

  showValue() {
    this.currentValue = this.touchSpin.getValue();
  }
}

Reactive Forms

import { Component } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
import { TouchSpinVanillaComponent } from '@touchspin/angular/vanilla';

@Component({
  selector: 'app-reactive',
  standalone: true,
  imports: [ReactiveFormsModule, TouchSpinVanillaComponent],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <label>
        Quantity:
        <touch-spin
          formControlName="quantity"
          [min]="1"
          [max]="99"
        ></touch-spin>
      </label>
      <button type="submit">Add to Cart</button>
    </form>
  `
})
export class ReactiveComponent {
  form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      quantity: [1]
    });
  }

  onSubmit() {
    console.log('Quantity:', this.form.value.quantity);
  }
}

Advanced Configuration

Custom Core Options

import { Component } from '@angular/core';
import { TouchSpinVanillaComponent } from '@touchspin/angular/vanilla';

@Component({
  selector: 'app-advanced',
  standalone: true,
  imports: [TouchSpinVanillaComponent],
  template: `
    <touch-spin
      [defaultValue]="50"
      [min]="0"
      [max]="100"
      [step]="1"
      [coreOptions]="{
        verticalbuttons: true,
        buttonup_class: 'custom-up',
        buttondown_class: 'custom-down'
      }"
    ></touch-spin>
  `
})
export class AdvancedComponent {}

Testing

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { TouchSpinVanillaComponent } from '@touchspin/angular/vanilla';

describe('TouchSpinComponent', () => {
  let component: TestComponent;
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [FormsModule, TouchSpinVanillaComponent, TestComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should increment value', () => {
    const touchSpin = fixture.nativeElement.querySelector('touch-spin');
    const incrementBtn = touchSpin.querySelector('button:last-child');

    incrementBtn.click();
    fixture.detectChanges();

    expect(component.value).toBe(6);
  });
});

@Component({
  selector: 'test-component',
  template: '<touch-spin [(ngModel)]="value" [defaultValue]="5"></touch-spin>'
})
class TestComponent {
  value = 5;
}

Development

# Install dependencies
yarn install

# Build all packages
yarn build

# Run tests
yarn test

# Run tests with coverage
yarn test:coverage

# Run tests in watch mode
yarn test:watch

# Type checking
yarn typecheck

# Linting
yarn lint

Related Packages

Core

  • @touchspin/core - Core TouchSpin logic and API

Renderers

  • @touchspin/renderer-vanilla - Vanilla CSS renderer
  • @touchspin/renderer-bootstrap3 - Bootstrap 3 renderer
  • @touchspin/renderer-bootstrap4 - Bootstrap 4 renderer
  • @touchspin/renderer-bootstrap5 - Bootstrap 5 renderer
  • @touchspin/renderer-tailwind - Tailwind CSS renderer

Adapters

  • @touchspin/angular - Angular adapter (this package)
  • @touchspin/react - React adapter
  • @touchspin/jquery - jQuery plugin
  • @touchspin/webcomponent - Web Components
  • @touchspin/standalone - Standalone bundle

Contributing

Contributions welcome! Please see the main TouchSpin repository for contribution guidelines.

License

MIT © Istvan Ujj-Meszaros