Home

Awesome

<p align="center"> <img width="20%" height="20%" src="./logo.svg"> </p> <br />

Making sure your tailor-made error solution is seamless!

MIT commitizen PRs styled with prettier All Contributors ngneat spectator

The Error Tailor offers seamless handling of form errors, saving you the trouble of repeating the error boilerplate. It's fully customizable, so you can control when, where, and how each form field's errors are displayed. Sit back, relax, and let the Error Tailor do all the work!

<img src="./demo.gif">

<a href="https://www.buymeacoffee.com/basalnetanel" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>

Getting Started

Run npm install @ngneat/error-tailor and add the imports to your application:

<!-- prettier-ignore-start -->
import { provideErrorTailorConfig } from '@ngneat/error-tailor';

bootstrapApplication(AppComponent, {
  providers: [
    provideErrorTailorConfig({
      errors: {
        useValue: {
          required: 'This field is required',
          minlength: ({ requiredLength, actualLength }) => 
                      `Expect ${requiredLength} but got ${actualLength}`,
          invalidAddress: error => `Address isn't valid`
        }
      }
    })
  ]
})
<!-- prettier-ignore-end -->

The errors config property takes a partial Provider, that should provide a HashMap<string | (err:any) => string> that is an object with keys corresponding to the errors name that you want to handle, and values that can be a simple string, or function that return a string used as error message to be shown. Now the only thing you need to add is the errorTailor directive to your form:

<form [formGroup]="form" errorTailor>
  <div class="form-group">
    <input class="form-control" formControlName="name" placeholder="Name" />
  </div>

  <section formGroupName="address">
    <div class="form-group">
      <input class="form-control" formControlName="city" placeholder="City" />
    </div>

    <div class="form-group">
      <input class="form-control" formControlName="country" placeholder="Country" />
    </div>
  </section>

  <div class="form-group">
    <select formControlName="animal" class="form-control">
      <option *ngFor="let option of options; index as index" [ngValue]="option">
        {{ option.label }}
      </option>
    </select>
  </div>

  <button class="btn btn-success">Submit</button>
</form>
import { errorTailorImports } from '@ngneat/error-tailor';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [errorTailorImports, ReactiveFormsModule]
})
export class AppComponent {
  private builder = inject(FormBuilder);
  form: FormGroup;

  ngOnInit() {
    this.form = this.builder.group({
      name: ['', [Validators.required, Validators.minLength(3)]],
      terms: [false, Validators.requiredTrue],
      animal: [null, Validators.required],
      address: this.builder.group(
        {
          city: ['', Validators.required],
          country: ['']
        },
        { validator: addressValidator }
      )
    });
  }
}

The directive will show all errors for a form field automatically in two instances - on the field element blur and on form submit.

Inputs

<input class="form-control" formControlName="city" 
       placeholder="City" controlErrorsClass="my-class other-class" />
       
<input class="form-control" formControlName="city" 
       placeholder="City" controlCustomClass="my-custom-class other-custom-class" />
       
<form errorTailor>
  <ng-template let-error let-text="text" #tpl> {{ error | json }} {{ text }} </ng-template>

  <div class="form-group">
    <input class="form-control" ngModel required name="name" [controlErrorsTpl]="tpl" />
  </div>

  <button class="btn btn-success">Submit</button>
</form>
<div class="form-check form-group">
  <input type="checkbox" formControlName="terms" id="check" [controlErrorAnchor]="anchor" />
  <label class="form-check-label" for="check">
    I agree to the terms and conditions
  </label>
  <ng-template controlErrorAnchor #anchor="controlErrorAnchor"></ng-template>
</div>

The custom anchor can also be added as a directive, in which case it'll act as the anchor for any nested form fields:

<div class="form-check form-group" controlErrorAnchor>
  <input type="checkbox" formControlName="terms" id="check" />
  <label class="form-check-label" for="check">
    I agree to the terms and conditions
  </label>
</div>
<input class="form-control" formControlName="country" placeholder="Country"
       [controlErrors]="extraErrors" />

One typical case when to use it is radio buttons in the same radio group where it's enough to show only one error message and not all of them for each separate radio button.

<div class="form-group">
  Communication language: &nbsp;
  <input type="radio" name="languages" formControlName="languages" 
         value="en" id="en"    [controlErrorAnchor]="anchorRadio" />
  <label class="form-radio-label" for="en">English</label>
  <input type="radio" name="languages" formControlName="languages" 
         value="de" id="de" controlErrorsIgnore />
  <label class="form-radio-label" for="de">German</label>
  <input type="radio" name="languages" formControlName="languages" 
         value="cs" id="cs" controlErrorsIgnore />
  <label class="form-radio-label" for="cs">Czech</label>

  <ng-template controlErrorAnchor #anchorRadio="controlErrorAnchor"></ng-template>
</div>
<input [controlErrorsOnAsync]="false" formControlName="name" />
<input [controlErrorsOnBlur]="false" formControlName="name" />
<input [controlErrorsOnBlur]="false" [controlErrorsOnAsync]="false" formControlName="name" />
<input [controlErrorsOnChange]="true" formControlName="name" />

Methods

Syntax as @ViewChild('gdprErrorTailor', { static: true }) gdprErrorTailor: ControlErrorsDirective; is used to get the reference and later call this.gdprErrorTailor.showError().

<input type="checkbox" formControlName="gdpr" #gdprErrorTailor="errorTailor" />

CSS Styling

The library adds a form-submitted to the submitted form. You can use it to style your inputs:

.form-submitted input.ng-invalid,
.form-submitted select.ng-invalid {
  border-color: #dc3545;
}

Config

{
  blurPredicate(element) {
    return element.tagName === 'INPUT' || element.tagName === 'SELECT';
  }
}
  // Custom error component that will replace the standard DefaultControlErrorComponent.
  @Component({
    standalone: true,
    imports: [errorTailorImports],
    template: `
    <ion-item lines="none" class="ion-text-wrap" [class.hide-control]="hideError">
      <ion-label color="danger" class="ion-no-margin ion-text-wrap" stacked>
        {{ errorText }}
      </ion-label>
    </ion-item>
    `
  })
  export class IonicControlErrorComponent extends DefaultControlErrorComponent {
  }

bootstrapApplication(AppComponent, {
  providers: [
    provideErrorTailorConfig({
      errors: {
        useValue: {
          required: 'This field is required'
        }
      },
      controlErrorComponent: IonicControlErrorComponent
    })
  ]
})
  anchorIonicErrorComponent(hostElement: Element, errorElement: Element) {
    hostElement.parentElement.insertAdjacentElement('afterend', errorElement);
    return () => {
      let errorNode = hostElement.parentElement.querySelector('custom-control-error');
      if (errorNode) {
        errorNode.remove();
      }
    };
  }

bootstrapApplication(AppComponent, {
  providers: [
    provideErrorTailorConfig({
      errors: {
        useValue: {
          required: 'This field is required'
        }
      },
      controlErrorComponent: IonicControlErrorComponent,
      controlErrorComponentAnchorFn: anchorIonicErrorComponent
    })
  ]
})

{
  controlErrorsOn: {
    async: true,  // (default: true)
    blur: true,   // (default: true)
    change: true, // (default: false)
  }
}

Recipes

I18n Example

Here's how to support i18n:

import { TranslocoService } from '@ngneat/transloco';

bootstrapApplication(AppComponent, {
  providers: [
    provideErrorTailorConfig({
      errors: {
        useFactory(service: TranslocoService) {
          return {
            required: error => service.translate('errors.required')
          };
        },
        deps: [TranslocoService]
      }
    })
  ]
})

Control Error Style

Here's a default style you can use for the error component:

.control-error {
  width: 100%;
  margin-top: 0.25rem;
  font-size: 12px;
  color: #dc3545;
}