Input fields with dynamic width in Angular

Have you ever typed text into small input field and did not see the whole text at once? It can be really frustrating. Textareas can be natively resized in most browsers today, but plain old input fields can not.

To address this I have written small Angular directive. See it in action in the video below or try the Stackblitz demo.

Live demo

Tricky details of implementation

Browsers are becoming more and more standard and predictable. But I’ve run into weird behaviour in Firefox. The inputs width did not correspond to the textual content. Reason? Different implementations of Window.getComputedStyle() . Chrome returns also the “shortcut CSS properties”, such as “font”. But Firefox returns only the specific properties, such as font-family, font-weight, font-variant etc. The same goes for other composed properties, such as border etc. Be sure to always use the most specific variant, otherwise you might run into trouble.

Available on npm

Yes, my first npm package! The world is now a better place. Get it here or by command yarn add dynamic-input-width or npm -i dynamic-input-width.

Usage

  • Install it ūüôā
  • Import DynamicInputModule in app.module.ts
  • add directive attribute to input/textarea element, like this <input [dynamicWidth]="{ minWidth: 10, maxWidth: 200 }" placeholder="dynamic width"/>
yeah!

Useful Typescript / Angular code snippets

Below are some useful code snippets I wrote. I use them as a reference, because they when I occasionally need them, I never know them by heart. Hope this will help you too. If so, let me know in the comments.

Convert enum to array

 export function enumToArray<T>(enumObj: object): T[] {
  const enumKeys: string[] = Object.keys(enumObj);
  const enumValues = enumKeys.map((key) => key as any).map((v) => enumObj[v] as any);
  return enumValues as T[];
} 

Set all controls in Angular dynamic form to readonly

 export function setFormReadonly(form: FormGroup, isReadonly: boolean) {
  Object.keys(form.controls).forEach((key) => {
    const control = form.controls[key];
    if (isReadonly) {
      control.disable();
    } else {
      control.enable();
    }
  });
} 

Get unique values of array

Works for array of primitive values only

 export function getUniqueValues<T>(arr: T[]): T[] {
  return Array.from(new Set(arr));
} 

Strip HTML tags from string

 export function stripHtmlTags(html: string, options?: { preserveWhitespace: boolean }): string {
  if (!html) {
    return html;
  }

  let startWhitespace = '';
  let endWhitespace = '';

  if (options?.preserveWhitespace) {
    const startWhitespaceResult = /^\s/.exec(html);
    const endWhitespaceResult = /\s$/.exec(html);
    startWhitespace = startWhitespaceResult ? startWhitespaceResult[0] : '';
    endWhitespace = endWhitespaceResult ? endWhitespaceResult[0] : '';
  }

  const doc = new DOMParser().parseFromString(html, 'text/html');
  return `${startWhitespace}${doc.body.textContent}${endWhitespace}` || '';
} 

Replace all occurences of given string

 export function replaceAll(str: string, find: string, replace: string) {
  return str.replace(new RegExp(find, 'g'), replace);
} 

Is empty object

 export function isEmptyObject(obj: Object): boolean {
  return !obj || (Object.keys(obj).length === 0 && obj.constructor === Object);
} 

Is same day

 export const isSameDay = (dateA: Date, dateB: Date): boolean => {
  const dayA = new Date(dateA.getFullYear(), dateA.getMonth(), dateA.getDate());
  const dayB = new Date(dateB.getFullYear(), dateB.getMonth(), dateB.getDate());
  return dayA.getTime() === dayB.getTime();
}; 

Delayed hover event in Angular

Use case:

Display tooltip only after user has been hovering for some time already. If he leaves sooner, then do not display anything.

Technologies

Angular, RxJs, Ngx UntilDestroy

Authors

David Votrubec and LuńŹek Cakl

Implementation

The logic behind is that we combine two streams. One stream is for the mouseenter event, and the other is for mouseleave. Then we map the events to boolean values. True means that we want to emit event, false means cancel.

The magic is in the combination of merge() and switchMap(). Merge listens to both streams, and returns single boolean observable. If it is false, we just return false. If it is true, we emit after delay. If there comes another false value, the delayed observable will not fire.

To clean up, there is untilDestroyed().

import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { untilDestroyed } from 'ngx-take-until-destroy';
import { fromEvent, merge, of } from 'rxjs';
import { delay, map, switchMap } from 'rxjs/operators';

@Directive({
	// tslint:disable-next-line:directive-selector
	selector: '[delayed-hover]',
})
export class DelayedHoverDirective implements OnInit, OnDestroy {
	@Input()
	delay = 1500;

	@Output('delayed-hover') hoverEvent = new EventEmitter();

	constructor(private readonly element: ElementRef) {}

	ngOnInit() {
		const hide$ = fromEvent(this.element.nativeElement, 'mouseleave').pipe(map(_ => false));
		const show$ = fromEvent(this.element.nativeElement, 'mouseenter').pipe(map(_ => true));

		merge(hide$, show$)
			.pipe(
				untilDestroyed(this),
				switchMap(show => {
					if (!show) {
						return of(false);
					}
					return of(true).pipe(delay(this.delay));
				})
			)
			.subscribe(show => {
				if (show) {
					this.hoverEvent.emit();
				}
			});
	}

	ngOnDestroy() {}
}

How to use it

<li (delayed-hover)="showTooltip()" delay="1500" (mouseout)="hideTooltip()"> ...</li>

Ctrl-click directive in Angular

Angular has lot of built-in directives for listening for events like click, dblclick, mousein, mouseout etc. But sometimes you need something more sophisticated, like detect ctrl-click.

I did not find any directive which would suit my needs, so I had to write my own. Be aware that normal browser behaviour for ctrl-click is to open link in new tab. So you need to be careful, where you use this, as you might confuse the user.

Feel free to remix / reuse the directive as needed. Comments are most welcome.

Happy coding

import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output, Renderer2 } from '@angular/core';

@Directive({
	// tslint:disable-next-line:directive-selector
	selector: '[ctrl-click]',
})
export class CtrlClickDirective implements OnInit, OnDestroy {
	private unsubscribe: any;

	// tslint:disable-next-line:no-output-rename
	@Output('ctrl-click') ctrlClickEvent = new EventEmitter();

	constructor(private readonly renderer: Renderer2, private readonly element: ElementRef) {}

	ngOnInit() {
		this.unsubscribe = this.renderer.listen(this.element.nativeElement, 'click', event => {
			if (event.ctrlKey) {
				event.preventDefault();
				event.stopPropagation();
				// unselect accidentally selected text (browser default behaviour)
				document.getSelection().removeAllRanges();

				this.ctrlClickEvent.emit(event);
			}
		});
	}

	ngOnDestroy() {
		if (!this.unsubscribe) {
			return;
		}
		this.unsubscribe();
	}
}

Gotcha

You need to be aware of one possible drawback. You can not reliably listen for both (click) and (ctrl-click) on the same element. The (click) event will fire every time. If you need both event listeners, then you need to nest them, like this:

<div (click)="reactToClick()">
 <span (ctrl-click)="reactToControlClick()"></span>
</div>

Provide username and password for git clone

There are situations when you need to clone some privately hosted git repository, which is accessible only via http. (Yes, I know it is not secure, but that was not my choice).

Ssh access is not provided, and neither is https. The only access is via http accessible on VPN. The connection is protected with username and password, but when trying to clone it, I have always got Authentication Error. Git never asked for credentials.

Solution

So how do you supply the required credentials? You can embed them directly in the url, like this

git clone http://username:password@rest.of.the.url

The obvious downside is that now your username/password are recorded in the console history.

Happy coding

Image result for clone wars meme

Awaitable http(s) request in Nodejs and Pulumi

Pulumi is a great tool for quick and easy deployment to cloud. It provides nice abstraction above several cloud providers, such as AWS, Kubernetes or Azure. But it also has some drawbacks. The biggest plus is that you can define your lambdas, tasks etc in single index.js file and import dependencies from other npm packages. Pulumi will then gather all those dependencies and compile then into single script. That script is then deployed to the could.

The drawback is that Pulumi can not compile some native functions, such as¬†[Symbol.iterator]. When you rely on some 3rd party npm package, which iterates with the help of Symbol.iterator, Pulumi will not be able to compile it. (Unfortunately, I can’t¬†google that error, but I’ve encountered it several times.)

My case was that I wanted to use async/await together with sending https request in nodejs. I’ve tried several npm packages which worked locally, but did not work in Pulumi. So after long struggle I came up with my own promise-based wrapper around http(s) requests. Feel free to use it. If you find it useful, please let me know in the comments.

const https = require("https");

module.exports.sendHttpsRequest = function(options, payload) {
 
    return new Promise(function (resolve, reject) {
        try {
 
            let body = [];
            const request = https.request(options, function (res) {
 
                if (res.statusCode != 200) {
                    reject(res.statusCode);
                }
 
                res.on('data', (data) => {
                  body.push(data);
                });

                res.on('end', () => {
                    // at this point, `body` has the entire request body stored in it as a string
                    let result = Buffer.concat(body).toString();
                    resolve(result);
                    return;
                });

                res.on('error', async (err) => {
                    console.error('Errooooorrrr', err.stack);
                    console.error('Errooooorrrr request failed');
                    reject(err);
                    return;
                });     
            });
 
            if (payload) {
                request.write(payload);
            };
 
            request.end();
        }
        catch (e) {
            console.log('Something unexpected', e);
        } 
    });
 }

Example of usage

async function testRequest() {
    const url = 'flickr.photos.search';

    const flickrResponse = await sendHttpsRequest({
        host: 'api.flickr.com',
        path: url,
        method: 'GET'
    }, null);

    return JSON.parse(flickrResponse);
}

Happy coding!

pulumi-sticker

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.