Angular validation common functions

Angular validation common functions

Taming Angular?forms

There are some repetitive validation functions that I would like to move for their own common file, after which, I want to turn the validation function into another unobtrusive pattern.

The final result should look like this

<cr-input placeholder="placeholder">
  <input crinput ... validator="functionName" />
</cr-input>
        

Let’s create popular validators and see what it takes.


Read the series on Sekrab Garage

Date

The date and time fields are handled by browsers pretty well; they don’t need a lot of work. What we need to cover in our patterns is future dates, past dates, and date ranges.

// final result I want to work with
<cr-input placeholder="When?">
  <input crinput type="date" id="appointment" formControlName="appointment" 
    validator="future" />
  <ng-container helptext>Future date</ng-container>
</cr-input>
        

Let’s change validator attribute into a validation function. We will follow the same method of patterns.

// new validators.ts

export const futureValidator = (control: AbstractControl): ValidationErrors | null => {
  // date is yyyy-mm-dd, should be int eh future
  const today = Date.now();

  if (!control.value) return null;
  const value = new Date(control.value);

  if (!value || +value > +today) {
    return null;
  }
  return {
    future: true
  };
};

// create a map to use globally
export const InputValidators = new Map<string, ValidatorFn>([
  ['future', futureValidator],
]);
        

In the directive, we’ll check if the validation function already exists. If not, we’ll add it.

// input.directive

// add a new input
@Input() validator?: string;

// in validate handler, just add it
validate(control: AbstractControl): ValidationErrors | null {

  if (this.validator) {
    const _validator = InputValidators.get(this.validator);
    if (_validator && !control.hasValidator(_validator)) {
      control.addValidators(_validator);
    }
  }
  // ... 
}  
        

There is another way to access the form input. However, it relies on having formControl defined and casting an Abstract control to a Form control. It’s ugly. And dependent on the form itself. I’m not using.

That’s it. We don’t need to validate. Then we just need to add a custom error message every time. For now that’s fine.

<cr-input placeholder="When?" error="Required and must be in the future">
  <input crinput type="date" id="appointment" formControlName="appointment" 
    validator="future" [required]="true" />
  <ng-container helptext>Future date</ng-container>
</cr-input>
        

A past date is straight forward.

Enrich the inline validator

We can improve it a bit, let’s pass parameters unobtrusively too, like this

<input crinput type="date" id="birthdate" formControlName="birthdate" validator="pastFn"
[params]="{date: someDate}"  />

someDate = new Date(2000, 1, 1);
        

That is what I call garnish, let’s add it, but let’s be very careful. If we ever have to fix a bug in it, we roll back to good old custom validation. I fixed a couple as I was writing this article! I suggest you implement your favorite method.

 // input.directive
@Input() validator?: string;
@Input() params?: any;

validate(control: AbstractControl): ValidationErrors | null {
    if (this.validator) {
        const _validator = InputValidators.get(this.validator);
        if (_validator && !control.hasValidator(_validator)) {
            // if params: apply function (note here params is no longer optional)
            if (this.params) {
                control.addValidators(_validator(this.params));
            } else {
                control.addValidators(_validator);
            }
        }
    }
    // ...
}
        

Then in our validators collection, wrap the function in another function to pass the arguments. Notice how I choose to name it with Fn suffix, to remind myself it's a function that needs params. (We might enhance by making the params optional and have a different check, but I am not well motivated to do that.)

// vaidtaors.ts

// example pastvalidation with date
export const pastValidatorFn = (params: {date: string}): ValidatorFn => {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) return null;

    const _date = makeDate(params.date);
    if(!_date) return null;

    const value = new Date(control.value);
    if (!value || +value < +_date) {
      return null;
    }
    return {
      past: true
    };
  };
};

// add to collection, the second argument of map is "any"
// if you want to reach 50s without a heart attack
export const InputValidators = new Map<string, any >([
    // ...
  ['pastFn', pastValidatorFn],
]);
        

So far so good. The date range should now be easy, all we need to do is update the validation functions to accept two parameters, minimum and maximum. If one is null, it’s ever.

// validator.ts
// A generic function for date ranges, fix it as you see fit
export const dateRangeValidatorFn = (params: {minDate?: string, maxDate?: string}): ValidatorFn => {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) return null;

    // make two dates if one is null, the other takes over, if both null, return null.
    const _min = makeDate(params.minDate);
    const _max = makeDate(params.maxDate);
    if (!_min && !_max) return null;

    // if both exist, range
    // if only one exists, check against that
    const _minDate = _min ? +_min : null;
    const _maxDate = _max ? +_max : null;
    const value = +(new Date(control.value));

    const future = _maxDate ? value < _maxDate : true;
    const past = value > _minDate; // null is also zero, so this works
    if (future && past) {
      return null;
    }

    return {
      dateRange: true
    };
  };
};

// add to map
export const InputValidators = new Map<string, ValidatorFn>([
    // ...
  ['dateRangeFn', dateRangeValidatorFn],
]);
        

Since we are passing params as an object, there is enough dynamic behavior to it, but I haven’t fully tested it.

Form component

<cr-input placeholder="Date range" error="Out of range">
    <input crinput type="date" id="daterange" class="w100" formControlName="daterange" [params]="params" validator="dateRangeFn" ?/>
    <ng-container helptext>Between 1 Jan 2024 - 1 Jan 2025</ng-container>
</cr-input>

minDate = new Date(2024, 0, 1);
maxDate = new Date(2025, 0, 1);
params = { minDate: this.minDate, maxDate: this.maxDate };
        

A date range selector.

Oh no. Use easypick. By far the least obtrusive. I hope they continue the path of “keeping this dead simple”

Password

To do this properly in our new validators shared function, it needs to pass the other field value dynamically. So we start with a function and a param.

// in the new validators.ts
export const matchPasswordFn = (pwd: AbstractControl): ValidatorFn => {
  return (control: AbstractControl): ValidationErrors | null => {
    // get password and match, if equal return null
    if (control?.value === pwd?.value) {
      return null;
    }
    return {
      matchPassword: true
    };
  };
};
        

The two fields in our component before using the validator property looks like this

// compontent
template: `
  <cr-input placeholder="Password">
     <!-- update validity on change -->
     <input crinput type="password" id="pwd" (change)="fg?.get('pwd2')?.updateValueAndValidity()"
     formControlName="pwd" crpattern="password" [required]="true" />
    <ng-container helptext>Alphanumeric and special characters, 8 characters minimum</ng-container>
  </cr-input>

  <cr-input placeholder="Confirm password" error="Does not match" >
    <input crinput type="password" id="pwd2" formControlName="pwd2" [required]="true" />
    <ng-container helptext>Should match password</ng-container>
  </cr-input>`

  // in class, before 
  ngOnInit() {
    this.fg = this.fb.group({
      pwd: [''],
      // this won't work
      // pwd2: ['', matchPasswordFn(this.fg.get('pwd')],
      pwd2: ['']
    });

    // this will (import from validators)
    this.fg.get('pwd2').setValidators(matchPasswordFn(this.fg.get('pwd')));
  }
        

Few things to notice:

  • Setting the validator directly in the form builder won’t work, because we need to access the form itself, which is not yet available.
  • We could have done a cross-form validation!
  • But because we did not, we needed to update validity of pwd2, whenever pwd changed. This is just to be as flexible as one can be.

We can import the matchPasswordFn directly from our validators function, but we can also use the validator property as follows:

// component

<cr-input placeholder="Confirm password" error="Does not match" >
    <!-- add the validator function directly -->
    <input crinput formControlName="pwd2" ...
        validator="matchPassword"
        [params]="fg?.get('pwd')"
    />
</cr-input>
        

And then the same validation function can be used.

Upload

We begin with a file input that is required. It does not look great, but I will not force it to look any different because every project will has its own unique look for this field. Some might also allow drag and drop, or paste. So we’ll focus on the basics.

Format and required validations are easy. We can create a general image pattern in our patterns list. Adding accept attribute is an additional friendly feature.

// patterns
export const InputPatterns = new Map<string, any>([
 //...
  ['image', '.+\\\\.{1}(jpg|png|gif|bmp)$'],
]);
        

The component looks like this

// component
<cr-input placeholder="Upload document">
  <input crinput type="file" id="file" formControlName="doc" crpattern="image"
  [attr.accept]="someArray" />
  <ng-container helptext>JPG, PNG only. 1 MB max.</ng-container>
</cr-input>
        

And it looks like this

The size is a bit harder to capture. But we can still use what we have. A new validation function that accepts two parameters, one for the maximum size, and one for the file size.

// validators
// validate file size to be what?
export const sizeValidatorFn = (params: {size: number, max: number}): ValidatorFn => {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) return null;
    // convert max from KB to bytes
    const _max = params.max * 1024;
    if (params.size > _max) {
      return {
        size: true
      };
    }

    return null;
  };
};

export const InputValidators = new Map<string, any >([
 // ...
  ['sizeFn', sizeValidatorFn]
]);
        

And to use it, we need something like this

// component
template: `<cr-input placeholder="Upload document" error="Invalid format or size">
  <input crinput type="file" id="file" class="w100" formControlName="doc" crpattern="image"
   #f validator="sizeFn" [params]="fparams" (change)="updateSize(f)" />
  <ng-container helptext>JPG, PNG only. 1 MB max.</ng-container>
</cr-input>`

fparams: { size: number, max: number };
this.fg = this.fb.group({
  doc: [],
});
updateSize(f: HTMLInputElement) {
    // update the params, then update validity
    // because files is not an immidiate property of formControl
    this.fparams.size = f.files[0]?.size;
    this.fg.get('doc').updateValueAndValidity();
}
        

This works as expected, but I think we can do better. The file input field is usually not part of the form, because the trend is to upload the file first to the cloud, then update our server record with the returned file unique identifier. We'll leave that to one fine Tuesday.

At least one

Let's add the "at least one" for group of checkboxes in there too.

// validators.ts
export const atleastOne = (control: AbstractControl): ValidationErrors | null => {
? // if all controls are false, return error

? const values = Object.values(control.value);
? if (values.some(v => v === true)) {
? ? return null;
? }

? return { atleastOne: true };
};
        

Then use directly in a group of checkboxes

// form component
<cr-input placeholder="Colors" error="At least one color" >
? ? <div formGroupName="colors" crinput validator="atleastOne">
      <label>
        <input type="checkbox" name="colors" id="color1" formControlName="red">
        Red
      </label> 
      <br>
      <label>
        <input type="checkbox" name="colors" id="color2" formControlName="black">
        Black
      </label> <br>
      <label>
        <input type="checkbox" name="colors" id="color3" formControlName="green">
        Green
      </label> <br>
    </div>
</cr-input>
        

That's enough.

Styling

In order to fine tune styling without making assumptions about the project style, nor using too smart CSS selectors, let's go back to few elements and dig deeper. Last episode. ??

Did you speak up today?

References


要查看或添加评论,请登录

Amal Ayyash的更多文章

  • Putting Angular Fire Firestore library to use?-?II

    Putting Angular Fire Firestore library to use?-?II

    Angular Firebase Firestore In this series, we're going to setup a project that uses Angular Fire library to list…

  • Putting Angular Fire Firestore library to use - I

    Putting Angular Fire Firestore library to use - I

    In this series, we're going to setup a project that uses Angular Fire library to list, create, edit and delete…

  • Validation style final tweaks

    Validation style final tweaks

    Taming Angular forms To add the missing styles to make it as functional as possible, need to keep reminding ourselves…

  • Angular form field types

    Angular form field types

    Taming Angular Forms Today we are going to dig into validating all field types. We begin with the simpler ones: the…

  • Angular form validation directive

    Angular form validation directive

    Taming Angular forms Series: Angular forms Creating a form has always been a terrible task I keep till the end. And I…

  • Taming Angular Forms

    Taming Angular Forms

    Using CSS to validate Angular reactive form I want to create a simple reusable solution for forms in Angular. A…

  • Upgrading to Angular version 19

    Upgrading to Angular version 19

    Angular SSR update, and deprecated tokens There are two ways to update, using directly, or creating a new application…

    1 条评论
  • The year 2022 in one word, one more word

    The year 2022 in one word, one more word

    YET ONE MORE ARTICLE ABOUT THE YEAR 2022 Webb. I remember that name for one reason only.

  • Creating a loading effect using RxJs in Angular

    Creating a loading effect using RxJs in Angular

    Carrying on from creating our state management based on RxJS, today we will create an example usage: a progress…

  • Angular 15 standalone HTTPClient provider: Another update

    Angular 15 standalone HTTPClient provider: Another update

    Another update, or maybe it was there but buried deep in documentation, to allow us to use HttpClient Module-less in…

社区洞察

其他会员也浏览了