Monday, February 24, 2020

Array Intersection in JavaScript

As JavaScript  progresses we continue to see new features an enhancements that make it into a more robust language. As we apply other languages and frameworks on top of JavaScript we get even more functionality (duh, that's why we use those frameworks). One thing that doesn't exist quite yet out of the box in JavaScript is array intersection. What I mean is the ability to see when one array contains values from another array.

For example, let's say our application has role-based permissions that get populated as an array. That array might look like this:const roles = ['employee', 'manager']; We protect something in our app by applying restrictions to it, maybe like this:const restrictions = ['president', 'accountant']; What we need to know is does the user have the right role to lift the restriction. In other words, we want to know if the two arrays intersect.

Since there is no Array.intersect (yet) we have to come up with something. This is just what I use when I have two simple arrays (in this case two arrays of string values). We could add it to the Array prototype if we want, but I'm not going to show that here.

const roles = ['employee', 'manager'];
const restrictions = ['president', 'accountant'];
const matches = roles.filter(r => restrictions.includes(r));


Once this runs, matches contains all of the values that were in both arrays (so it's an empty array in this case). We can do evaluations on it like:
if (!!matches && !!matches.length) {
  // do some stuff
}

BONUS: We can do the same thing with complex objects by applying the map function to this process. So if our array of roles looked like this:const roles = [{name: 'employee'}, {name: 'manager'}]; we could do the intersect like this:const matches = roles.map(r => r.name).filter(role => restrictions.includes(role));

I got this one and a couple of other goodies from this post on Medium. Happy coding!

Tuesday, February 11, 2020

Masked Input Length Validation in Angular

We use angular2-text-mask to mask some of our inputs at work (so phone number fields show parentheses and a hyphen, for example). I discovered today that using the built-in Angular minLength validator doesn't work with the text mask because the masked value meets the length criteria. So I wrote my own validator based on the built-in one that accepts an optional array of characters to replace and then only returns true if the modified length (that is, the length of the string after removing masking characters) is long enough (or short enough, depending on which validator you use). You know what? Let me just show you.

   1:  import { Injectable } from '@angular/core';
   2:  import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
   3:  
   4:  @Injectable({
   5:    providedIn: 'root'
   6:  })
   7:  export class MaskedLengthValidator {
   8:    static minLength(minLength: number, replace?: Array<string>): ValidatorFn {
   9:      return (control: AbstractControl): ValidationErrors | null => {
  10:        if (this.isEmptyInputValue(control.value)) {
  11:          return null;
  12:        }
  13:  
  14:        let value = control.value;
  15:  
  16:        if (!!replace && !!replace.length) {
  17:          replace.forEach(c => {
  18:            var regEx = new RegExp(c, 'g');
  19:            value = value.replace(regEx, "");
  20:          });
  21:        } else {
  22:          value = control.value.replace(/_/g, "");
  23:        }
  24:  
  25:        const length: number = value ? value.length : 0;
  26:        return length < minLength ?
  27:          {'minlength': {'requiredLength': minLength, 'actualLength': length}} :
  28:          null;
  29:      };
  30:    }
  31:  
  32:    static maxLength(maxLength: number, replace?: Array<string>): ValidatorFn {
  33:      return (control: AbstractControl): ValidationErrors | null => {
  34:        let value = control.value;
  35:  
  36:        if (!!replace && !!replace.length) {
  37:          replace.forEach(c => {
  38:            var regEx = new RegExp(c, 'g');
  39:            value = value.replace(regEx, "");
  40:          });
  41:        } else {
  42:          value = !!value ? control.value.replace(/_/g, "") : value;
  43:        }
  44:  
  45:        const length: number = value ? value.length : 0;
  46:        return length > maxLength ?
  47:          {'maxLength': {'requiredLength': maxLength, 'actualLength': length}} :
  48:          null;
  49:      };
  50:    }
  51:  
  52:    static isEmptyInputValue(value: any): boolean {
  53:      // we don't check for string here so it also works with arrays
  54:      return value == null || value.length === 0;
  55:    }
  56:  }
  57:  

So that's the validator. The simple way to use it is like this:
   1:    ngOnInit() {
   2:      this.someForm = this.formBuilder.group({
   3:        phoneNumber: [null, [Validators.required, MaskedLengthValidator.minLength(10), MaskedLengthValidator.maxLength(10)]]
   4:      });
   5:    }

And if you have placeholders in your mask other than underscores you can use it like this:
   1:    ngOnInit() {
   2:      this.someForm = this.formBuilder.group({
   3:        phoneNumber: [null, [Validators.required, MaskedLengthValidator.minLength(10, ['-']), MaskedLengthValidator.maxLength(10, ['-'])]]
   4:      });
   5:    }

This is pretty straightforward once you think about it. Now you don't have to think about.