0%

Introduction

In Angular, there are several ways to bind data to the view, including text interpolation, property binding, class/style bindings, and event binding. In this article, we’ll cover the basics of these bindings.

In an Angular template, a binding creates a live connection between a part of the UI created from a template(a DOM element, directive, or component) and the model(the component instance to which the template belongs). This connection can be used to synchronize the view with the model, to notify the model when an event or user action takes place in the view, or both. Angular’s Change Detection algorithm is responsible for keeping the view and the model in sync.

Text interpolation

Text interpolation refers to embedding expressions into marked up text, Angular use {{}} to interpolate expressions into HTML.

Suppose you have a variable name in the component, you can use it in the template like this:

1
<p>Hello, {{name}}!</p>

Or if you want to display an image based on its URL, you can do this:

1
<img src="{{imageUrl}}" alt="image">

You can also use conditional expressions in text interpolation, for example:

1
<div>Performance: {{ score > 90 ? 'Exceeded' : 'Match' }}</div>

Or even a function call:

1
<div>Performance: {{ getPerformance() }}</div>

Note: text interpolation only works on string, for all other types, Angular will first convert it to string then interpolate it.

Property binding

Property binding in Angular helps you set values for properties of HTML elements or directives.

Property binding moves a value in one direction, from a component’s property into a target element property. In the following example, the src property of the img element is bound to the imageUrl property of the component.

angular-property-binding

Property binding syntax

You should enclose the HTML element property with [], and put the component’s property in "". In this way angular will parse the content in quotes as a variable. the following code won’t work, since angular will treat imageUrl as a literal string, not a variable in component.

1
<img src="imageUrl" alt="image">

Text interpolation can also be used interchangeably with property binding, the following code has the same effects.

1
<img src="{{imageUrl}}" alt="image">

So, if imageUrl is an variable in component class, then
[src]="imageUrl" is equal to src="{{imageUrl}}".

Class/Style bindings

Class bindings

If you only need single class/style binding, you can use the following syntax.
Where grade is a variable in the component, and this works as long as grade is a truthy value.

1
<div [class.grade]="grade">Student A</div>

class bindings works the same as ngClass directive, see here for details.

Style bindings

Style bindings works like class bindings, see here, we rarely use style bindings, since we always put styles into a single file, but its good to know.

References

https://angular.dev/guide/templates

Introduction

ngClass is a directive in Angular that allows you to add or remove classes on an HTML element.

Usage

First, create a Student component in your app.

1
ng generate component student

1. import NgClass from @angular/common;

1
import {NgClass} from "@angular/common";

2. Add NgClass to the imports array in the component/module;

We’re using standalone component here.

1
2
3
4
5
6
7
@Component({
selector: 'app-student',
templateUrl: './student.component.html',
standalone: true,
imports: [NgClass], // <--- here
styleUrls: ['./student.component.css']
})

3. Use the ngClass directive in the HTML template.

We’ll categorize students based on their grades, different grades will have different colors.

ngClass accepts string, array, object, map, set, function, and binding to a CSS class. we’ll use all of them in the following examples.

1. String syntax

Note, since ngClass is a property binding, we need to wrap the class name in '' to make it a hard-coded string, otherwise the class name will be treated as a variable in your component.

1
<div [ngClass]="'grade-A'">Student A</div>

ngClass is commonly used to bind a variable in component, for example, we can define a variable grade in the component, and bind it to the ngClass directive.

1
grade = 'grade-A';
1
<div [ngClass]="grade">Student A</div>

If you want to apply a class without a condition, you can simply use the following syntax.

1
<div class="grade-A">Student A</div>

2. Array syntax

1
<div [ngClass]="['grade-A', 'grade-B']">Student B</div>

3. Object syntax

1
<div [ngClass]="{'grade-A': true, 'grade-B': false}">Student C</div>

4. Map syntax

1
<div [ngClass]="gradeMap">Student D</div>
1
2
3
4
gradeMap = new Map<string, boolean>([
['grade-A', true],
['grade-B', false]
]);

5. Set syntax

1
<div [ngClass]="gradeSet">Student E</div>
1
gradeSet = new Set<string>(['grade-A', 'grade-B']);

With any object-like bindings such as Array, Object, Map, and Set syntax, the identity of the object must change for Angular to apply the new class. Updating the property without changing the identify has no effect, for example:

This one doesn’t work, because gradeObject is the same object.

1
this.gradeObject["grade-b"] = false;

This one works, because gradeObject is a new object.

1
this.gradeObject = {...this.gradeObject, "grade-b": false};

Note that, Map and Set syntax are rarely used, object and array syntax are the most common.

6. Expression syntax

grade is a component variable, we can use it in the expression.

1
<div [ngClass]="grade === 'isGradeA' ? 'grade-A' : ''">Student F</div>

7. Function syntax

1
<div [ngClass]="getGrade()">Student F</div>
1
2
3
getGrade() {
return 'grade-A';
}

Difference between [ngClass] and [class]

[ngClass] is a directive that allows you to add or remove classes based on conditions, while [class] is a property binding that allows you to add a class without a condition.

Most of the above case can be achieved by [class] as well, except the following case.

This one works

1
<div [ngClass]="{'grade-a grade-b': true}">Student</div>

But this one doesn't work

1
<div [class]="{'grade-a grade-b': true}">Student</div>

References

From Quora: https://www.quora.com/What-is-the-most-elegant-piece-of-JavaScript-code-you-can-write

I actually have two candidates for the title of “most powerful line of code ever written in JavaScript”. The first is this line:

1
element.innerHTML = "..."; 

The innerHTML property was introduced by Microsoft in the Internet Explorer 4.0 browser (IE4), which was released in October 1997. Prior to this, the only way to modify the HTML content of a page from JavaScript was using the document.write method. Unfortunately, calling document.write on a loaded document automatically calls document.open, which clears the entire document. In other words, it was only with the introduction of innerHTML that JavaScript gained the ability to modify a specific portion of a page, by inserting a bit of arbitrary HTML inside an existing element. And thus, modern Web applications were born.

My other candidate is this line:

1
var xhr = new XMLHttpRequest(); 

Prior to this, content could only be downloaded into the browser by requesting a whole new page. Developers resorted to all sorts of tricks, primarily using hidden frames, to circumvent this limitation, but such code was very unwieldy, and had problematic side-effects. It was Microsoft again that changed the game by introducing the XMLHttpRequest object in Internet Explorer 5.0 (IE5) in March 1999. And just like that, AJAX was created, and along with it Single-Page Applications, and the modern Web we know today.

How logical or || operator works in JavaScript

Consider the following expression, what’s the evaluated result?

1
`expr1 || expr2`

If expr1 can be converted to true, return expr1. Otherwise, return expr2.
So we have the following truth:

1
2
3
4
5
6
7
8
9
10
true || true; // t || t returns true
false || true; // f || t returns true
true || false; // t || f returns true
false || 3 === 4; // f || f returns false
"Cat" || "Dog"; // t || t returns "Cat"
false || "Cat"; // f || t returns "Cat"
"Cat" || false; // t || f returns "Cat"
"" || false; // f || f returns false
false || ""; // f || f returns ""
false || varObject; // f || object returns varObject

|| in javascript not always return boolean(true or false)

Note that || in javascript is quit difference from || in other languages. For example, in Java, || returns Boolean(true or false), but in JavaScript, it returns the value of one of the operands.

1
2
"" || 1; // returns 1
false || ""; // returns ""

If you want to convert the value to boolean, you can use double !! or Boolean()

  • !!expr - convert to boolean
  • Boolean(expr) - convert to boolean

Short circuit

|| is a short-circuit operator, which means if the first operand is true, it will not evaluate the second operand.

1
true || console.log("Hello"); // nothing output

|| vs ??

|| and ?? are both used to provide a default value, but they have different behaviors, if you only care about null or undefined, and treat other Falsy value as valid(for example 0 or empty string "") use ??, otherwise, use ||.

1
2
3
4
5
6
7
const x = 0;
const y = x || 10; // y = 10
const z = x ?? 10; // z = 0

const a = "";
const b = a || "default"; // b = "default"
const c = a ?? "default"; // c = ""

References

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR
  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_assignment

In JavaScript, the nullish operator ?? is used to return the right-hand side value if the left-hand side value is null or undefined. Otherwise, it returns the left-hand side value.

1
2
3
4
5
6
7
8
x ?? y // return x if x is not null or undefined, otherwise return y

0 ?? 100 // 0
"" ?? "default" // ""
null ?? "default" // "default"
undefined ?? "default" // "default"
null ?? undefined // undefined
undefined ?? null // null

Difference between || and ??

  1. || returns the first truthy value among its operands, if all operands are falsy, it returns the last operand.
  2. ?? returns the first non-null and non-undefined value among its operands, if all operands are null or undefined, it returns the last operand.

Short-circuiting evaluation.

Same as ||, ?? also supports short-circuiting evaluation. If the left-hand side value is not null or undefined, the right-hand side value will not be evaluated.

Have you ever seen this error: “TypeError: xxx is not a function”? This error occurs when you try to call a function that does not exist.

1
2
3
4
5
6
const person = {
name: 'Philip',
age: 18,
};

person.getAge(); // TypeError: person.getAge is not a function

You can solve this by the optional chaining operator ?.:

1
person.getAge?.(); // nothing output

However, if there is a property in the object has the same name by accident, you will still get that error

1
2
3
4
5
6
7
const person = {
name: 'Philip',
age: 18,
getAge: 18,
};

person.getAge?.(); // TypeError: person.getAge is not a function

Test promise

How many ways to test an async function which has a promise with jest in an angular app? suppose you have the following function

1
2
3
4
5
6
7
getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(10);
}, 0);
});
}

To test this function, you can use the following ways:

1. Using async/await

1
2
3
4
it('should return 10', async () => {
const result = await getData();
expect(result).toBe(10);
});

2. By returning promise

In jest, when you return a promise in your test, jest will wait for the promise to resolve. see here for details.

1
2
3
4
5
it('should return 10', () => {
return getData().then(result => {
expect(result).toBe(10);
});
});

3. By a done callback

This one works fine when promise is resolved. but if the promise is rejected, the test will wait for a maximum of 5 seconds timeout, and then fail. while others will fail immediately.

1
2
3
4
5
6
it('should return 10', (done) => {
getData().then(result => {
expect(result).toBe(10);
done();
});
});

4. By waitForAsync in angular

In angular, you can use waitForAsync to test async functions. Note that you don’t need await in this case.

1
2
3
4
5
it('should return 10', waitForAsync(() => {
getData().then(result => {
expect(result).toBe(10);
});
}));

5. Using fakeAsync in angular

In angular, you can use fakeAsync to test async functions. Note that you don’t need flush or tick in this case.

1
2
3
4
5
6
it('should return 10', fakeAsync(() => {
getData().then(res => {
expect(result).toBe(10);
});
flush();
}));

6. This one not work!

Since we have a setTimeout in the getData function, the following test will fail.

1
2
3
4
5
it('should return 10', () => {
getData().then(result => {
expect(result).toBe(10);
});
});

if getData() doesn’t have a setTimeout, the above test will work.

1
2
3
4
5
getData() {
return new Promise((resolve, reject) => {
resolve(10);
});
}

Test observable

How many ways to test an async function which returns an observable with jest in an angular app? suppose you have the following function

1
2
3
4
5
6
7
getData() {
return new Observable((observer) => {
setTimeout(() => {
observer.next(10);
}, 1000);
});
}

1. Test with done callback

1
2
3
4
5
6
it('should complete with value 10', (done) => {
component.getData().subscribe((result) => {
expect(result).toBe(10);
done();
});
});

2. Test by convert observable to promise

1
2
3
it('should complete with value 10', async () => {
await expect(firstValueFrom(component.getData())).resolves.toBe(10);
});

3. Convert to promise manually

This one is not recommended, Please use firstValueFrom or lastValueFrom from rxjs to convert observable to promise.

1
2
3
4
5
6
7
8
it('should complete with value 10', () => {
return new Promise((resolve, reject) => {
component.getData().subscribe((result) => {
expect(result).toBe(10);
resolve(true);
});
});
});

In this post, I will show you how to set up a Jest project from scratch. We won’t use any front-end frameworks like React, Angular, or Vue, just a simple vanilla JavaScript project.

CommonJs

Step 1: Create a new project

Run the following command to init a new project directory, it will simply create a package.json file in the current directory.

1
npm init -y # with -y, you don't need to answer the questions

Step 2: Install Jest

Run the following command to install Jest as a dev dependency.

1
npm install --save-dev jest

Step 3: Create a javascript file

We’ll first focus on CommonJs, create a new file named sum.js in the root directory of your project, and add the following code:

1
2
3
4
function sum(a, b) {
return a + b;
}
module.exports = sum;

Step 4: Create a test file

Create a new file named sum.test.js in the root directory of your project, and add the following code:

1
2
3
4
5
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

Step 5: Update package.json

Add the following line to the scripts property in the package.json file:

1
2
3
"scripts": {
"test": "jest"
}

Step 6: Run the test

Run the following command to run the test:

1
npm test

Run test with coverage

Run the following command to run the test with coverage:

1
npx jest -- --coverage

ESModule

To run Jest with ESModule, first rewrite your file and test file to use ESModule syntax.

1
2
3
4
// sum.js
export default function sum(a, b) {
return a + b;
}
1
2
3
4
5
6
// sum.test.js
import sum from './sum.js';

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

Run the test, you will got following error:

1
2
Jest encountered an unexpected token
SyntaxError: Cannot use import statement outside a module

This is because you are trying to use ESModule syntax in a CommonJs environment, to fix this, you can update your file name to sum.test.mjs or you need to add the following line to the package.json file:

1
2
3
4
"jest": {
"moduleFileExtensions": ["js", "mjs"],
"transform": {}
}

Then run the test again, it still not work, got the following error, it seems that Jest didn’t find any test file with the default pattern.

1
2
3
4
5
6
7
8
No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In D:\personal\codes\jest
5 files checked.
testMatch: **/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x) - 0 matches
testPathIgnorePatterns: \\node_modules\\ - 5 matches
testRegex: - 0 matches
Pattern: - 0 matches

Update jest config in package.json file to include mjs file extension in testMatch, see micromatch for detail matching rules. Note: this is not regex.

1
2
3
4
5
"jest": {
"testMatch": [
"**/?(*.)test.?js"
]
},

Then run the test again, got the previous error again

1
SyntaxError: Cannot use import statement outside a module

Then search jest official documentation, change jest command in package.json file to:

1
2
3
4
5
"scripts": {
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
}

Then run the test again, it works now.

Using config file

You can move the jest config in package.json to a separate file named jest.config.js, and update the package.json file to use the config file.

1
2
3
4
"scripts": {
"test": "jest --config jest.config.js"
"test-esm": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js"
}

Then create a new file named jest.config.js in the root directory of your project, and add the following code:

1
2
3
4
5
module.exports = {
moduleFileExtensions: ['js', 'mjs'],
transform: {},
testMatch: ['**/?(*.)test.?js'],
};

Then run the test again, it works now.

You don’t need to mock localStorage with latest jsdom

Suppose you have a localStorage utility function like this:

1
2
3
4
5
6
7
8
9
10
11
12
// storage-utils.ts
export function saveItem(item: any) {
localStorage.setItem('item', JSON.stringify(item));
}

export function getItem(name: string) {
const jsonString = localStorage.getItem(name);
if (jsonString) {
return JSON.parse(jsonString);
}
return null;
}

To test above code, you have the following test file:

1
2
3
4
5
6
7
8
9
10
11
// storage-utils.spec.ts
import { getItem, saveItem } from './storage-utils';

describe('local storage should work', () => {
it('should set and get item', () => {
const item = { name: 'John Doe', age: 30 };
saveItem(item);
const storedItem = getItem('item');
expect(storedItem).toEqual(item);
});
});

When you run this test with Jest

1
2
# first, navigate to the directory where the test file is located.
jest storage-utils.spec.ts

Everything works fine, but localStorage is only available in browser environment, and you didn’t mock it, why?

This is because Jest now use jsdom as the default environment, and jsdom has implemented localStorage and sessionStorage APIs, so you don’t need to mock them anymore.

To test this, you can find the jest.config.js file in the root directory of your project, and add the following line to the testEnvironment property:

1
2
3
4
5
module.exports = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
testEnvironment: 'node', // <-- change this to 'node'
};

Then run the test again, you will see the test failed because localStorage is not available in the node environment, and you will get the following error:

1
2
localStorage is not defined
ReferenceError: localStorage is not defined

Use jsdom by in test file

Suppose you global jest config is node(in file jest.config.js under project root), but you want to use jsdom in some test cases, you can use the following code in the test file, add @jest-environment jsdom at the top of the file.(Don’t use // comment, it will not work)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @jest-environment jsdom
*/

import { getItem, saveItem } from './storage-utils';

describe('local storage should work', () => {
it('should set and get item', () => {
const item = { name: 'John Doe', age: 30 };
saveItem(item);
const storedItem = getItem('item');
expect(storedItem).toEqual(item);
});
});

Mock localStorage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// mockLocalStorage.ts
const mockLocalStorage = (() => {
let store = {} as Storage;

return {
getItem(key: string) {
return store[key];
},

setItem(key: string, value: string) {
store[key] = value;
},

removeItem(key: string) {
delete store[key];
},

clear() {
store = {} as Storage;
},
};
})();

export default mockLocalStorage;

Test file, note that we use globalThis here, it’s environment independently, and works both in node and browser environment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// storage-utils.spec.ts
import { getItem, saveItem } from './storage-utils';
import mockLocalStorage from './mockLocalStorage';

describe('local storage should work', () => {
beforeEach(() => {
Object.defineProperty(globalThis, 'localStorage', {
value: mockLocalStorage,
});
});

it('should set and get item', () => {
const item = { name: 'John Doe', age: 30 };
saveItem(item);
const storedItem = getItem('item');
expect(storedItem).toEqual(item);
});
});

References:

  1. https://jestjs.io/docs/configuration#testenvironment-string - the official document still said that node is the default environment, but in my case, it’s jsdom, not sure why.
  2. https://github.com/jsdom/jsdom/blob/main/Changelog.md#11120