File validators in Angular

Whenever you allow users to upload files to your server, you should have both client side and server side validation. Most likely you will validate file size and file type, identifiable by the file extension. FileValidator covers all three aspects. Example of usage is below the source code.

If you are using Angular, you can use these simple validators. If you do use them, or find them inspiring, please let me know.

import { ValidatorFn, FormControl } from '@angular/forms';

export class FileValidator {

 static fileMaxSize(maxSize: number): ValidatorFn {
    const validatorFn = (file: File) => {
      if (file instanceof File && file.size > maxSize) {
        return { fileMinSize: { requiredSize: maxSize, actualSize: file.size, file } };
      }
    };
    return FileValidator.fileValidation(validatorFn);
  }

  static fileMinSize(minSize: number): ValidatorFn {
    const validatorFn = (file: File) => {
      if (file instanceof File && file.size < minSize) {
        return { fileMinSize: { requiredSize: minSize, actualSize: file.size, file } };
      }
    };
    return FileValidator.fileValidation(validatorFn);
  }

  /**
   * extensions must not contain dot
   */
  static fileExtensions(allowedExtensions: Array<string>): ValidatorFn {
    const validatorFn = (file: File) => {
      if (allowedExtensions.length === 0) {
        return null;
      }

      if (file instanceof File) {
        const ext = FileValidator.getExtension(file.name);
        if (allowedExtensions.indexOf(ext) === -1) {
          return { fileExtension: { allowedExtensions: allowedExtensions, actualExtension: file.type, file } };
        }
      }
    };
    return FileValidator.fileValidation(validatorFn);
  }

  private static getExtension(filename: string): null|string {
    if (filename.indexOf('.') === -1) {
      return null;
    }
    return filename.split('.').pop();
  }

  private static fileValidation(validatorFn: (File) => null|object): ValidatorFn {
    return (formControl: FormControl) => {
      if (!formControl.value) {
        return null;
      }

      const files: File[] = [];
      const isMultiple = Array.isArray(formControl.value);
      isMultiple
        ? formControl.value.forEach((file: File) => files.push(file))
        : files.push(formControl.value);

      for (const file of files) {
        return validatorFn(file);
      }

      return null;
    };
  }

}

Example of usage

class FileDetailExampleComponent {
 this.allowedExtensions = ['csv', 'xls'];

 this.form = this.fb.group({
      dataFile: ['', [FileValidator.fileMinSize(1), FileValidator.fileExtensions(this.allowedExtensions)]],
      headerFile: ['', [FileValidator.fileMinSize(1), FileValidator.fileExtensions(this.allowedExtensions)]]
    });
}

Happy coding!

Angular (click) not fired and how to fix it

Sometimes, very rarely, you can run into situation then Angular does not fire the (click) event. Or it fires later, on the 2nd click. It can be really hard to debug and figure out what causes it.

So here are my two cents. This can be caused by view being updated while user clicks. What happens under the hood is this: Click event consists of mousedown event followed by mouseup event. That is important to remember! But when the view changes, then the mouseup event is fired elsewhere, so the click event never happens. It can even be, that the re-rendered button is the same exact position, and visually nothing has changed. But internally it is different DOM element.

To sum it up: Click event is actually two events combined on the same element, mousedown and mouseup.

The hotfix is then obvious, bind to (mousedown) event, which will surely fire, when the user clicks it.

huh-wtf-was-that

<!-- hotfix -->
<div (mouseDown)="clickHandler()"></div>

How to migrate Pulumi stack

Pulumi is a great abstraction layer above AWS and other cloud infrastructure. It enables to quickly deploy and update your cloud. But what if you have several AWS profiles, eg: several private ones and one or more which belongs to a company? Your private one is the default and you use it to deploy … Aaaah, problems, the deployed code is not hosted under your company account 🙂

To migrate perform these steps:

  1. pulumi destroy  // destroy current stack under personal account
  2. set AWS_PROFILE=your-company-profile // define global env variable read by both Pulumi and AWS CLI
  3. pulumi up -y // create stack under company profile

The important takeaway is to check the value of AWS_PROFILE. Otherwise you might accidentaly deploy somewhere else and the infra will not be accessible to your colleagues.

Dockerizing Angular/Web3 app

Web development these days is often more about combining various npm packages then actuall programming. And the packages incompactibility can be extremely frustrating at times. When you add crypto to this mix, it becomes even wilder. Truffle framework promises to ease some problems with deployment of smart contracts, but it introduces other hard-to-debug problems. Recently I started developing small dApp on Angular and I’ve run into yet another issues.

1st issue – can not install web3 on Windows

This is old one, and there is no 100% reliable solution. You will be recommended to install windows-build-tools, but the installation process itself can hang/crash and then you are stuck.

// unfortunatelly this can still fail
npm install --global --production windows-build-tools
node-gyp configure --msvs_version=2015
npm config set python /path/to/executable/python2.7
npm install web3 --save

2st issue – package crypto built in nodejs is not available

The package has been integrated into nodejs from version 11. So if you are using node v10, you will run into trouble as some npm packages depend on it. In order to avoid the hassle with nvm and rebuilding globally installed packages, you can dockerize it.

How to dockerize Angular app?

Add this Dockerfile to the root folder of your project

FROM node:11.1.0

RUN echo node version
RUN node --version

# set working directory
RUN mkdir /usr/src/app
WORKDIR /usr/src/app

# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH

# increment this line when you update the package.json
# otherwise Docker will you cached version of this layer
RUN touch break-cache-15

# install and cache app dependencies
COPY package.json /usr/src/app/package.json
RUN npm install -g @angular/cli
RUN npm install

# add app
COPY . /usr/src/app

# enable crypto package in nodejs
COPY patch.js /usr/src/app/patch.js
RUN node patch.js

# Expose the port the app runs in
EXPOSE 4200

# start app
# the poll option should overcome problem with not working module hot-reload
CMD ng serve --host 0.0.0.0 --poll 1000

And the .dockerignore file to speed things up

node_modules
npm-debug.log
.git

Build the image

docker build -t your-image-name .

And run it

docker run -d --name container-name -v c:\src\path-to-project:/usr/src/app:rw -v /usr/src/app/node_modules -p 4200:4200 your-image-name

Optimize workflow

As you develop, and add/remove npm packages, you will want to re-create docker images and run new versions of container. Repeating the same commands over and over is tedious, so I recommend create aliases or shortcuts. On Windows you can create them with the doskey command. Described fully in this article.

Insert script tags in Angular components

Sometimes you just need to insert 3rd party script tags into Angular Components. It is definitely not very common, and Angular does not make it easy for you. What ever script tag you define your component’s template, will be simply cleared out. Not even a html comment will remain. So I you really need to insert a 3rd party script into your component, you have to do inside the javascript part.

It is actually quite simple. You create a script DOM element, set the src or innerHTML or text property and attach it to the DOM via renderer service.

// 1. import dependencies
import { Renderer2, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';

// 2. pass then in constructor
constructor(
    private renderer2: Renderer2,
    @Inject(DOCUMENT) private _document
  ) {
  }

// 3. call them in ngOnInit
ngOnInit() {
   const s = this.renderer2.createElement('script');
   s.type = 'text/javascript';
   s.src = 'https://path/to/your/script';
   s.text = ``;
   this.renderer2.appendChild(this._document.body, s);
}

But what if you have scripts, which depend on each other, and have to be called only after the previous ones has been loaded? For example you load affiliate tracker, and you need to initialize it. If you call the initialization code too soon, you will get an error. Luckily you can use the onload property of the script element, and use it to load the dependent scripts. So the final code would look something like this

// 3. call them in ngOnInit - using onload for dependencies
ngOnInit() {
   const s = this.renderer2.createElement('script');
   s.onload = this.loadNextScript.bind(this);
   s.type = 'text/javascript';
   s.src = 'https://path/to/your/script'; // Defines someGlobalObject
   s.text = ``;
   this.renderer2.appendChild(this._document.body, s);
}

loadNextScript() {
   const s = this.renderer2.createElement('script');
   s.text = `
   // This would error, if previous script has not yet been loaded
    someGlobalObject.doSomething();
`
   this.renderer2.appendChild(this._document.body, s);
}

Happy coding. Hope this helped someone

debugging

What to read more?

Dive deeper into Angular Components in this article on Toptal blog. How to use them, difference between components and directives, performance etc.

Workaround for eternal problem with node-gyp on Windows

This is super annoying. Reliance on node-gyp on Windows is such a pain! The error description says the it can not find the Python executable, even though it is there.

There is a workaround – 2021 SOLUTION

  • Re-Install node-gyp globally – npm install -g node-gyp@latest 
  • Delete node-modules
  • Delete package lock file
  • Reinstall all packages again (pnpm i or npm i)
  • PS: I recommend using pnpm instead of npm. It is much faster.
  1. OLD WORKAROUND FROM 2018
  2. Open cmd as Administrator (Otherwise the installation fails, meh)
  3. run command npm install –global –production windows-build-tools
  4. This will install Visual Studio Build Tools (yes, I know, there is mismatch in names)
  5. This will also install Python again as a side-effect (meh)
  6. run npm rebuild in the project’s folder
  7. re-open cmd and run npm install
  8. all should be fine now

npm hell
npm hell

Convert exponential numbers to decimal form in Javascript

As almost every developers knows, Javascript has notorious problems when working with really big or really small decimal numbers. The reason is floating point. Purpose of this article is not to explain why it behaves the way it does, there are much better resources dedicated just for that. You can read the explanation here or here. There are also various libraries dedicated to working with decimals and precise calculations, such as big.js.

To ilustrate the problem, see the short video below. The input shows the text value, and on the right there is raw value as represented in Javascript. The issue it can cause, is that when Angular (or any other framework for that sake) re-renders the value, it uses the raw value. So the 0.00000001 is replaced with 1e-8. Not very user friendly.

So as a workaround, I’ve written simple utility function. It takes a number and returns correctly formatted string. Be aware that because how Javascript handles decimal numbers, you are not able to represent small fractions in their decimal form. Therefore it has to be represented as a string or some kind of object.

/**
 * Checks if number is in exponential format (eg: 1e-8 for 0.00000001).
 * If it does not, original number is returned.
 * If it does it converts it to string representation of that number
 * which forces it to format 0.00000001
 */
export function convertExponentialToDecimal(exponentialNumber: number): number|string {
  // sanity check - is it exponential number
  const str = exponentialNumber.toString();
  if (str.indexOf('e') !== -1) {
    const exponent = parseInt(str.split('-')[1], 10);
    // Unfortunately I can not return 1e-8 as 0.00000001, because even if I call parseFloat() on it,
    // it will still return the exponential representation
    // So I have to use .toFixed()
    const result = exponentialNumber.toFixed(exponent);
    return result;
  } else {
    return exponentialNumber;
  }

If it helped you solve your problem, let me know in comments below

Decimal number is not a number!

You have an input field, where user can enter decimal values. Problem is, that some cultures use decimal point “.” and some uses decimal comma “,”. So what is a valid number in one culture, is not a number at all in a different culture. You can see the full list of differences on wikipedia.

There is no built-in validator for numbers in Angular, so we have built our own like this:

import { AbstractControl, ValidatorFn } from '@angular/forms';

export class NumberValidators {

  static isNumber(): ValidatorFn {
    return (c: AbstractControl): { [key: string]: boolean } | null => {
      if (isNaN(c.value)) {
        return { 'isNumber': true };
      }
      return null;
    };
  }
}

Can you see, where the problem is? It uses isNaN() to check if given value is number or not. But isNaN() does not accept number with decimal comma as valid.

isNaN('10.0');
// logs 'false'
isNaN('10,0')
// logs 'true'

As usual there are many ways how to do validate numeric values, I could rewrite the validator to allow both variants, but then I would have to handle both variants on server. Which I do not want. So the solution is to use a directive, which will automagically convert decimal comma to decimal point before validation kicks in.

The directive works like this:

  1. Subscribe to keydown and paste events
  2. Check if the new value contains decimal comma
  3. If yes, suppress the event and
  4. Replace comma with point
  5. Then triggers native DOM event again

I use customRenderer to dispatch native DOM events, which naturally bubbles up the DOM tree. Angular then reacts to that event and validation kicks in. Here is the source code.

import { AfterViewInit, Directive, ElementRef, OnDestroy } from '@angular/core';
import { CustomRenderer } from 'app/utils/custom-renderer';
import { replaceAll } from 'app/utils/replace-string';
import { fromEvent } from 'rxjs/observable/fromEvent';

/**
 * Detects when user press key "decimal point"
 * which depends on user's locale.
 * In some locales it prints decimal point "."
 * and in other it prints decimal comma ","
 * This directive forces that decimal comma is always translated to decimal point.
 */
// Full list of countries using either decimal point or comma can be found here https://en.wikipedia.org/wiki/Decimal_separator
@Directive({
  selector: '[decimalPoint]'
})
export class DecimalPointDirective implements AfterViewInit, OnDestroy {

  private input: HTMLInputElement;
  private alive = true;

  constructor(
    private el: ElementRef,
    private customRenderer: CustomRenderer
  ) {
  }

  ngAfterViewInit() {
    this.input = this.el.nativeElement;

    fromEvent(this.input, 'keydown')
    .takeWhile(() => this.alive)
    .subscribe((event: KeyboardEvent) => {
      if (event.key === ',') {
        event.preventDefault();
        event.stopPropagation();
        this.input.value = this.input.value + '.';
        this.triggerEvent('input');
      }
    });

    fromEvent(this.input, 'paste')
    .takeWhile(() => this.alive)
    .subscribe((event: Event) => {
      const pastedValue = <string>(<any>event).clipboardData.getData('text/plain');

      if (pastedValue.indexOf(',')) {
        event.preventDefault();
        event.stopPropagation();
        const fixedValue = replaceAll(pastedValue, ',', '.');
        this.input.value = fixedValue;
        this.triggerEvent('input');
      }
    });
  }

  // Trigger dom native event, which bubbles up as usual.
  private triggerEvent(eventName: string) {
    this.customRenderer.invokeElementMethod(this.el, 'dispatchEvent', [new CustomEvent(eventName, {bubbles: true})]);
  }

  ngOnDestroy() {
    this.alive = false;
  }
}

Example usage

<input type="number" formControlName="amount" decimalPoint>

Angular – .map is not a function

I’ve encountered really weird error. After I’ve removed clearly unused code, our Angular app stopped working. It did however pass lint tests and also the production build passed without problems. The error occurred only during runtime. The error said something like “map is not a function at blablabla” and on the first glance it was totally unrelated to the code where it occurred.

After long digging and googling I found the root cause. Somewhere in our app we used import ‘rxjs/RX’, which imported the whole RxJS library which is large. It has about 50+ operators. And because it was included in the bundle accidentally, the app magically worked. We never had to worry about manually importing the RxJs operators. That is until we fixed the unused and unrelated import, hehe.

Conclusion

So the conclusion is, never import rxjs/RX because it will increase your bundle code size. But include only the specific operators which you really use. You can do it in the main module (usually called the app.module.ts) or specifically in those files, where they are actually used.

 

totally unrelated image :)

My import statement in app.module.ts now looks like this

import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/bufferWhen';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/publish';
import 'rxjs/add/operator/publishLast';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/takeWhile';
import 'rxjs/add/operator/timeout';
import 'rxjs/add/operator/withLatestFrom';

Which is much better than this

// never ever!
import 'rxjs/Rx';

Invoke native element method with Angular 6

The useful method invokeElementMethod() has been removed from the new Renderer v3 in the upcoming version 6 of Angular. What does it mean? For example it is now harder to invoke focus() on input field or create custom native event, which bubbles up. The motivation of the Angular team is that this method has not been used internally, therefore they have removed it. I don’t think this was the best solution, because this method has been used by many.

So now when you need to focus some input field, you need to write up your own renderer service. It makes the things more complicated without obvious benefit. Or am I missing something?

You can create your own injectable renderer service like this

import { Injectable, PLATFORM_ID, Inject, ElementRef } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable()
// Based on https://github.com/angular/angular/issues/13818#issuecomment-372276031
// and source code of the old Renderer V1
export class CustomRenderer {

  constructor(
    @Inject(PLATFORM_ID) private platformId: Object
  ) { }

  invokeElementMethod(eleRef: ElementRef, method: string, args: any[]) {
    if (isPlatformBrowser(this.platformId)) {
      let element = eleRef.nativeElement;
      element[method].apply(element, args);
    }
  }
}

You can use it to create native DOM elements which will bubble-up. That is sometimes easier, than having chain of EventEmmiters across a tree of components. You can create custom event like this

@Component({
  selector: 'app-demo',
  templateUrl: './demo.component.html',
  styleUrls: ['./demo.component.scss']
})
export class DemoComponent implements OnInit {
 constructor(private elRef: ElementRef, private customRenderer: CustomRenderer) { }

 this.customRenderer.invokeElementMethod(
  this.elRef, 
  'dispatchEvent', 
  [new CustomEvent('nameOfCustomEvent', {bubbles: true})]
 );
}

Hope this helps someone