# ng-error-tooltips

[![npm version](https://badge.fury.io/js/ng-error-tooltips.svg?icon=si%3Anpm)](https://badge.fury.io/js/ng-error-tooltips)
![build status](https://github.com/mkeller1992/ng-error-tooltips/actions/workflows/npm-publish.yml/badge.svg)
[![codecov](https://codecov.io/gh/mkeller1992/ng-error-tooltips/graph/badge.svg?token=FDYFIOR4LQ)](https://codecov.io/gh/mkeller1992/ng-error-tooltips)

An Angular library for **Reactive Forms** and **Signal Forms** that displays tooltips on form inputs with errors.

The latest library version is compatible with **Angular 22**.  
Starting with version **20.1.0**, `ng-error-tooltips` is fully **zoneless-compatible**.

## Migration note (Signal Forms)

Starting with version **21.3.x**, the `ErrorTooltipSigDirective` no longer reads the form field via `[formField]`.

You must now explicitly pass the field using `[errorTooltipField]`:

```html
<input
  [formField]="signalForm.name"
  ngErrorTooltipSig
  [errorTooltipField]="signalForm.name">
```

---

## Demo

https://mkeller1992.github.io/ng-error-tooltips/

---

## Install

```bash
npm i ng-error-tooltips
```

---

## Setup

### Standalone apps (ApplicationConfig / app.config.ts)

```ts
import { ApplicationConfig, provideZonelessChangeDetection, signal } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideErrorTooltipOptions, provideErrorTooltips, type SupportedLanguage } from 'ng-error-tooltips';
import { validate } from '@angular/forms/signals';

import { routes } from './app.routes';

// Optional: use a signal when the language can change at runtime.
export const demoLang = signal<SupportedLanguage>('de');

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideZonelessChangeDetection(),

    // lang is optional (defaults to 'de')
	// Pass either a static language string ('de', 'en' or 'fr') or a Signal<SupportedLanguage>.
    // validate is only required for Signal Forms / CustomSigValidators
    provideErrorTooltips({ lang: demoLang, validate }),

    // Optional global defaults for all tooltips.
    // Local [options] and explicit inputs like [placement] or [textColor] can still override these values.
    provideErrorTooltipOptions({
      textColor: '#7f1d1d',
      backgroundColor: '#fff7ed',
      borderColor: '#f97316',
    }),
  ],
};
```

Tooltip options are merged in this order:

```text
library defaults < provideErrorTooltipOptions(...) < [options] < explicit inputs
```

### Directives

```ts
import {
  ErrorTooltipDirective,
  ErrorTooltipSigDirective,
  ErrorTooltipSigFormDirective,
} from 'ng-error-tooltips';
```

---

## Usage

### Reactive Forms

Define a reactive form with validators in your TypeScript component.  
For applications with language switching support, use the `CustomValidatorsI18n` variants.

```ts
import { Component, inject } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ErrorTooltipDirective, CustomValidators } from 'ng-error-tooltips';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
  imports: [FormsModule, ReactiveFormsModule, ErrorTooltipDirective],
})
export class AppComponent {
  private readonly formBuilder = inject(FormBuilder);

  formGroup: FormGroup = this.formBuilder.group({
    nameInput: new FormControl<string>('', {
      validators: [
        CustomValidators.required(),
        CustomValidators.minLength(3),
      ],
    }),
  });
}
```

```html
<form [formGroup]="formGroup" (ngSubmit)="submit()">
  <input
    ngErrorTooltip
    formControlName="nameInput"
    placeholder="Enter your name*"
    type="text">

  <button type="submit">Submit</button>
</form>
```

### Signal Forms

```ts
import { Component, inject, signal, viewChild } from '@angular/core';
import { form, FormField, submit } from '@angular/forms/signals';
import { CustomSigValidators, ErrorTooltipSigDirective, ErrorTooltipSigFormDirective } from 'ng-error-tooltips';

interface Employee {
  name: string;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  imports: [FormField, ErrorTooltipSigDirective, ErrorTooltipSigFormDirective],
})
export class AppComponent {
  private readonly v = inject(CustomSigValidators);

  readonly ttForm = viewChild(ErrorTooltipSigFormDirective);

  readonly employee = signal<Employee>({
    name: '',
  });

  readonly signalForm = form(this.employee, path => [
    this.v.requiredI18n(path.name),
    this.v.minLengthI18n(path.name, 3),
  ]);

  async submit() {
    // marks all fields touched + runs validation
    await submit(this.signalForm, async () => undefined);

    if (!this.signalForm().valid()) {
      // show all tooltips inside the container
      this.ttForm()?.showErrorTooltips();
    }
	else {
		this.ttForm()?.hideErrorTooltips();
	}
  }
}
```

```html
<div ngErrorTooltipSigForm>
  <input
    [formField]="signalForm.name"
    ngErrorTooltipSig
    [errorTooltipField]="signalForm.name"    
    placeholder="Enter your name*"
    type="text">
</div>

<button type="button" (click)="submit()">Submit</button>
```

---

### Three ways to pass additional properties

1. Set global defaults for all tooltips via `provideErrorTooltipOptions(...)` in your `ApplicationConfig`, as shown above in **Setup > Standalone apps**:

```ts
provideErrorTooltipOptions({
  textColor: '#7f1d1d',
  backgroundColor: '#fff7ed',
  borderColor: '#f97316',
});
```

2. Pass one or more properties via an `ErrorTooltipOptions` object:

```ts
import { ErrorTooltipOptions } from 'ng-error-tooltips';

tooltipOptions: ErrorTooltipOptions = {
  placement: 'right',
  textColor: '#7f1d1d',
  backgroundColor: '#fff7ed',
  borderColor: '#f97316',
};
```

```html
<input
  formControlName="ageInput"
  ngErrorTooltip
  [options]="tooltipOptions"
  placeholder="Enter your age*"
  type="number">
```

3. Pass explicit inputs directly on the control:

```html
<input
  ngErrorTooltip
  [placement]="'right'"
  formControlName="nameInput"
  placeholder="Enter your name*"
  type="text">
```

**Note:** Tooltip options are merged as `library defaults < provideErrorTooltipOptions(...) < [options] < explicit inputs`.

---

## Internationalisation (i18n)

Starting with version **21.1.0**, `ng-error-tooltips` supports **reactive multi-language error messages**.

If you do nothing, the tooltip falls back to **German (`de`)** error messages.

To enable language switching, provide the current language as a `Signal<'de' | 'fr' | 'en'>` using `provideErrorTooltips`.

Whenever the language signal changes, all visible error tooltips update automatically.

---

## Properties

| name | type | default | description |
|-----|------|---------|-------------|
| id | string \| number | 0 | A custom id that can be assigned to the tooltip |
| showFirstErrorOnly | boolean | false | Whether the tooltip should only display the first error if multiple errors exist |
| placement | Placement | 'bottom-left' | The position of the tooltip |
| zIndex | number | 1101 | The z-index of the tooltip |
| tooltipClass | string | '' | Additional CSS classes applied to the tooltip (`::ng-deep`) |
| shadow | boolean | true | Whether the tooltip has a shadow |
| offset | number | 8 | Offset of the tooltip relative to the element |
| width | string | '' | Fixed width of the tooltip |
| maxWidth | string | '350px' | Maximum width of the tooltip |
| textColor | string | '' | Custom text color of the tooltip |
| backgroundColor | string | '' | Custom background color of the tooltip |
| borderColor | string | '' | Custom border color of the tooltip and arrow |
| pointerEvents | "auto" \| "none" | 'auto' | Whether the tooltip reacts to pointer events |
| appendTooltipToBody | boolean | true | Whether the tooltip should be appended to the document body. Set to `false` for Angular Material dialogs or other overlay-based components where body-appended tooltips may appear behind the overlay |

---

## Angular Vitest unit tests

### Mocking `ErrorTooltipDirective` (Reactive Forms)

```ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { ErrorTooltipDirective, MockErrorTooltipDirective } from 'ng-error-tooltips';
import { FormBuilder } from '@angular/forms';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent],
      providers: [FormBuilder]
    })
    .overrideComponent(AppComponent, {
      remove: { imports: [ErrorTooltipDirective] },
      add: { imports: [MockErrorTooltipDirective] }
    })
    .compileComponents();

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

### Mocking `ErrorTooltipSigDirective` and `ErrorTooltipSigFormDirective` (Signal Forms)

```ts
import { ErrorTooltipSigDirective, MockErrorTooltipSigDirective } from 'ng-error-tooltips';

await TestBed.configureTestingModule({
  imports: [AppComponent],
})
  .overrideComponent(AppComponent, {
    remove: { imports: [ErrorTooltipSigDirective, ErrorTooltipSigFormDirective] },
    add: { imports: [MockErrorTooltipSigDirective, MockErrorTooltipSigFormDirective] }
  })
  .compileComponents();
```

### Mocking custom validators

`MockCustomValidators` mirrors the static Reactive Forms validator API and always returns valid results:

```ts
import { MockCustomValidators } from 'ng-error-tooltips';

const form = new FormGroup({
  name: new FormControl('', [
    MockCustomValidators.required(),
    MockCustomValidators.minLength(3),
  ]),
});
```

`MockCustomSigValidators` mirrors the injectable Signal Forms validator service and can replace `CustomSigValidators` in Angular tests:

```ts
import { CustomSigValidators, MockCustomSigValidators } from 'ng-error-tooltips';

await TestBed.configureTestingModule({
  imports: [AppComponent],
  providers: [
    { provide: CustomSigValidators, useClass: MockCustomSigValidators },
  ],
}).compileComponents();
```
