Mock window.location in unit tests

When you run unit tests (I am using Jestjs) on functions which read window.location.href or window.location.search, you need to somehow mock the value.

Problem is that when you try to edit the value of window.location.search it will not work. Neither will work overriding it by Object.defineProperty(). But there is a simple workaround solution, and that is using history.pushState(). By that you can change the current URL to whatever value you need, and then read it by tested function.

Example

beforeEach(() => {
 const search = `a=aaaa&b=bbb`;
 window.history.pushState({}, 'Test Page Title', `/any/url/you/like?${search}`);
});

Coding drunk make programming fun again book | StareCat.com
Well… I just liked the image 🙂

Debugging NestJS app in NRWL NX workspace

Debugging can save you ton of time. But sometimes to set up debugging configuration correctly can be a nightmare. There are several common gotchas which may trick you.

  • Code is in Typescript, but NodeJS in debugger does not understand its syntax.
  • You use combination of .ts and .js files with import or export statement
  • Debugger does not attach, and breakpoints are ignored
  • False positive detection of debug mode (flag --inspect or --inspect-brk did not work for my use case)

NRWL NX is a great tool, but somewhat it complicated debugging of NestJS app. At least for me 🙂 So after a couple of hours of trial and error, this is what finally works for me.

launch.json

Notice that --experimental-modules is turned on. Also notice usage of pwa-node, and specification of env variables.

{
	"version": "0.2.0",
	"configurations": [
		{
			"name": "Debug my NESTJS app",
			"type": "pwa-node",
			"request": "launch",
			"args": ["src/main.ts"], // Path to main entry file
			"runtimeArgs": ["--require", "ts-node/register", "--require", "tsconfig-paths/register", "--experimental-modules"],
			"cwd": "${workspaceRoot}",
			"internalConsoleOptions": "openOnSessionStart",
			"env": {
				"NODE_ENV": "local",
				"NODE_PORT": "8000",
				"TS_NODE_PROJECT": "tsconfig.debug.json", // Specify the tsconfig to use. See content of it below.
				"IS_DEBUG_MODE": "true" // Custom env variable to detect debug mode
			},
			"sourceMaps": true,
			"console": "internalConsole",
			"outputCapture": "std",
			"resolveSourceMapLocations": [
				"${workspaceFolder}/**",
				"!**/node_modules/**" // Disable the "could not read source map" error for node_modules
			]
		}
	]
}

tsconfig.debug.json

{
	"extends": "./tsconfig.json",
	"compilerOptions": {
		"outDir": "../../dist/out-tsc",
		"types": ["node"],
		"emitDecoratorMetadata": true,
		"target": "es6",
		"module": "commonjs",
		"importHelpers": false,
		"allowJs": true // Fixes combination of .js and .ts files
	},
	"exclude": ["**/*.spec.ts"],
	"include": ["**/*.ts"]
}

Detection of debug mode inside running application

export function isInDebugMode(): boolean {
	// can not rely on "--inspect" flag, because that is used when launched via NX. Even in standard launching.
	// "--inspect-brk" flag did not work either
	// So in normal situation you would use this:
	// return /--inspect/.test(process.execArgv.join(' '));

	// Workaround using ENV variable
	return process.env['IS_DEBUG_MODE'] === 'true';
}

Fix of path to files in debug mode

Normally the asset files would be in workspace/dist/appname/assets folder. That applies when application is launched normally via nx serve appname command. But when you launch app in debug mode, then the __dirname variable may resolve differently, and then the app crashes.

So this is my workaround.

export class SomeFileService {
	private readonly encoding = 'utf8';

	// Different path in debug mode and standard mode
	private readonly pathToFile = isInDebugMode()
		? path.resolve('./', 'src/assets/configs/predefined-programs.json')
		: path.resolve(__dirname, 'assets/configs/predefined-programs.json');

        async readFile(): Promise<T> {
		return await readFile(this.pathToFile, this.encoding)
			.then((data) => {
				const parsedData = JSON.parse(data);
				return parsedData;
			})
			.catch((err) => {
				console.error('SomeFileService: Error reading data from ' + this.pathToFile, err);
			});
	}
}

Happy coding.

Rainbow text color in CSS

No images, this is pure CSS.

This is really funny. You can have rainbow text on gradient background purely in CSS. No images needed.
And yes, you can have radial background too. On a long text in looks funny and cool. Look, how awesome (and weird) it is. Mystical colors.

And here is the trick how to do it:

Combine background-image property with background-clip. Not much code required. Small caveat – don’t forget to specify ::selection too, otherwise selected text may not be visible.

/** rainbow letters */
 .rainbow-demo {
     background-image: linear-gradient(to left, violet, indigo, blue, green, #CCCC00, orange, red);
     -webkit-background-clip: text;
     -webkit-text-fill-color: transparent;
 }

 .rainbow-demo::selection {
     color: #b400ff;
     -webkit-text-fill-color: #b400ff;
 }
Taste the rainbow and be happy

Working with Angular i18n inside NRWL NX workspace

Angular v10 has great support for localization using the i18n attributes. I will not describe it, because that is already described in detail on official Angual site. What I found missing was how to integrate it with NRWL NX approach.

Problems encountered

For starters, the workspace.json has different structure than angular.json, so some parts are not 1:1. Workspace.json extends angular.json schema and adds its own properties to it. In its most basic form it is ok to just rename angular.json to workspace.json and update the version property. But the documentation about how to implement localization in NRWL NX workspace is very scarse (written in March 2021).

Nx serve cant serve multiple localizations

Angular i18n approach enables you to define multiple localization files, and then compile multiple instances of the app for each localization. Each localized version will live in different folder and will use different baseHref attribute.

Example

http://myapp.com/fr for French version of the app, 
and http://myapp.com/en for English version of the app 

Define multiple locales in default build target

You can define mutliple localizations in your workspace.json, and then build them all at one step.

"targets": {
  "build": {
    "executor": "@angular-devkit/build-angular:browser",
    "options": {
        // … options not relevant to i18n removed
        // 3) LOG WARNING ABOUT MISSING TRANSLATIONS
        "i18nMissingTranslation": "warning",
        // 4) LIST OF SUPPORTED LOCALIZATIONS
        // THIS WILL COMPILE DIFFERENT INSTANCES INTO "fr" AND "en" FOLDERS
        // BUT WILL PRODUCE PROBLEM IN "nx serve myapp" command
        "localize": ["fr", "en"]
    },

nx build appname will then produce those different folders.

But nx serve can not work multiple locales. For obvious reason, each language version is a different instance of the application. So you need to instruct nx serve which language to use.

Define named build targets for each locale

Named build target can be invoked when you build/serve your application, so that different configuration is used. The most used example is the --prod flag, which uses build target named “production“. We define named build targets for each locale, like this

"configurations": {
   "production": {
     // ... some prod settings
   },
   "en": {
     "localize": ["en"]
   },
   "fr": {
     "localize": ["fr"]
   }
  },

Update serve configuration to use specific build target

The trick is in the browserTarget option says which named build target to use. If you don’t specify it, then the default is used, but that contains multiple locales, and thus breaks the serve command.

"serve": {
 "executor": "@angular-devkit/build-angular:dev-server",
 "options": {
   "browserTarget": "appname:build:en"
 }
}

And thats it. It looks simple in retrospect, but it took me considerable time to figure it out. I hope this will save someones time. Or maybe time of my future self.

All relevant code together

// 1) NAME OF THE APPLICATION
 "appname": { 
    "projectType": "application",
    "root": "apps/appname",
    "sourceRoot": "apps/appname/src",
    "prefix": "appname",
    // 2) PATH TO LOCALIZATION FILES
    "i18n": {
      "locales": {
          "fr": "apps/appname/src/locale/messages.fr.xlf",
          "en": "apps/appname/src/locale/messages.en.xlf"
      }
    },
    "targets": {
      "build": {
          "executor": "@angular-devkit/build-angular:browser",
          "options": {
            // … options not relevant to i18n removed
            // 3) LOG WARNING ABOUT MISSING TRANSLATIONS
            "i18nMissingTranslation": "warning",
            // 4) LIST OF SUPPORTED LOCALIZATIONS
            // THIS WILL COMPILE DIFFERENT INSTANCES INTO "fr" AND "en" FOLDERS
            // BUT WILL PRODUCE PROBLEM IN "nx serve myapp" command
            "localize": ["fr", "en"]
          },
    "configurations": {
     "production": {
       // 5) HERE GOES PRODUCTION SETTINGS - IGNORED FOR BREWITY
     },
     // 6) DEFINITION OF LOCALE FOR NAMED BUILD TARGETS
     // USE "en" FOR BUILD TARGET "en"
     // AND USE "fr" FOR BUILD TARGET "fr"
     "en": {
       "localize": ["en"]
     },
     "fr": {
       "localize": ["fr"]
     }
    },
   "serve": {
        "executor": "@angular-devkit/build-angular:dev-server",
        "options": {
                "browserTarget": "appname:build:en"
        }
  }
On pull request to cripple them all. We have all been there …

Further reading

Tagged function $localize. This is really cool, you can translate messages directly in your code like this:

$localize`this message will be translated like in the template`

Angular animation trap

Animations in Angular.io are a nice feature. But their error messages can be quite misleading.

When you forgot to define the animations array in the @Component annotation, then Angular will report following error message:

“Found the synthetic property @detailExpand. Please include either “BrowserAnimationsModule” or “NoopAnimationsModule” in your application

That is clearly misleading, because the error occurs even then the BrowserAnimationsModule is actually included. What it should say instead is something like this: “You are trying to use @detailExpand, but there is no animation property on the component.” Also this error is not mentioned in the official guide.

So, next time you see this error, check the animations annotation. See the example below.

import { animate, state, style, transition, trigger } from '@angular/animations';
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';

@Component({
	selector: 'my-component',
	templateUrl: './my.component.html',
	styleUrls: ['./my.component.scss'],
	changeDetection: ChangeDetectionStrategy.OnPush,
	animations: [
		// see @detailExpand in the .html template
		trigger('detailExpand', [
			state('collapsed', style({ height: '0px', minHeight: '0' })),
			state('expanded', style({ height: '*' })),
			transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
		]),
	],
})
export class OrbpTableComponent implements OnInit, OnChanges {
 ...
}

<div class="my-component">
   <div class="example-element-detail" [@detailExpand]="row.id == expandedOrbpRowId ? 'expanded' : 'collapsed'">
     Animated content goes here
   </div>
</div>

Happy coding!

Angular debugging

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 &amp;&amp; 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