Skip to content

Commit

Permalink
feat: add host binding migration
Browse files Browse the repository at this point in the history
  • Loading branch information
ostromeckyp committed Feb 12, 2025
1 parent e86b7eb commit d69d0f5
Show file tree
Hide file tree
Showing 8 changed files with 659 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: Host Binding Migration
description: Schematics for migrating from decorator-based Host Bindings to the new host properties in Angular
entryPoint: plugin/src/generators/convert-host-binding
badge: stable
contributors: ['pawel-ostromecki']
---

Recent Angular releases recommend using the `host` property instead of the `@HostBinding` and `@HostListener` decorators, as these decorators are retained primarily for backward compatibility. This schematic will assist you in migrating your code to utilize the new `host` property.

### How it works?

The moment you run the schematics, it will look for all the decorators that have binding and replace them with `host` properties.

- It will keep the same name for the attributes and properties bindings.
- It will update the component's decorators by adding the `host` property if it does not exist or by adding additional properties within it.
- It won't convert properties to signals.
- It will remove the `@HostListener`, `@HostBinding` decorators.

### Example

Before running the schematics:

```typescript
import { Component, HostBinding, HostListener } from '@angular/core';

@Component({
/* ... */
})
export class CustomSlider {
@HostBinding('attr.aria-valuenow')
value: number = 0;

@HostBinding('tabIndex')
getTabIndex() {
return this.disabled ? -1 : 0;
}

@HostListener('keydown', ['$event'])
updateValue(event: KeyboardEvent) {
/* ... */
}
}
```

After running the schematics:

```typescript
import { Component } from '@angular/core';

@Component({
host: {
'[attr.aria-valuenow]': 'value',
'[tabIndex]': 'disabled ? -1 : 0',
'(keydown)': 'updateValue($event)',
},
})
export class CustomSlider {
value: number = 0;
disabled: boolean = false;
updateValue(event: KeyboardEvent) {
/* ... */
}
}
```

### Usage

In order to run the schematics for all the project in the app you have to run the following script:

```bash
ng g ngxtension:convert-host-binding
```

If you want to specify the project name you can pass the `--project` param.

```bash
ng g ngxtension:convert-host-binding --project=<project-name>
```

If you want to run the schematic for a specific component or directive you can pass the `--path` param.

```bash
ng g ngxtension:convert-host-binding --path=<path-to-ts-file>
```

### Usage with Nx

To use the schematics on a Nx monorepo you just swap `ng` with `nx`

Example:

```bash
nx g ngxtension:convert-host-binding --project=<project-name>
```
10 changes: 10 additions & 0 deletions libs/plugin/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"factory": "./src/generators/convert-to-sfc/generator",
"schema": "./src/generators/convert-to-sfc/schema.json",
"description": "libs/plugin/src/generators/convert-to-sfc/ generator"
},
"convert-host-binding": {
"factory": "./src/generators/convert-host-binding/generator",
"schema": "./src/generators/convert-host-binding/schema.json",
"description": "libs/plugin/src/generators/convert-host-binding/ generator"
}
},
"schematics": {
Expand Down Expand Up @@ -80,6 +85,11 @@
"factory": "./src/generators/convert-to-sfc/compat",
"schema": "./src/generators/convert-to-sfc/schema.json",
"description": "libs/plugin/src/generators/convert-to-sfc/ generator"
},
"convert-host-binding": {
"factory": "./src/generators/convert-host-binding/compat",
"schema": "./src/generators/convert-host-binding/schema.json",
"description": "libs/plugin/src/generators/convert-host-binding/ generator"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`convert-host-binding generator should convert properly for component 1`] = `
"
import { Component } from '@angular/core';
@Component({
selector: 'my-component',
template: 'My component',
host: { '[class.active]': 'isActive', '[attr.aria-disabled]': 'isDisabled', '[tabIndex]': 'getTabIndex()' }
})
export class MyComponent {
isActive = true;
get isDisabled() {
return true;
}
getTabIndex() {
return this.isDisabled ? -1 : 0;
}
}
"
`;

exports[`convert-host-binding generator should convert properly for component with complex host listener 1`] = `
"
import { Component } from '@angular/core';
@Component({
selector: 'my-complex-component',
template: 'My complex component',
host: { '(document:keydown.escape)': 'escapeKeydownHandler($event)', '(mousedown)': 'handleMousedown($event.target)', '(click)': 'handleEventNoArgs()' }
})
export class MyComplexComponent {
escapeKeydownHandler(event: KeyboardEvent): void {
}
handleMousedown(target: any): void {
}
handleEventNoArgs(): void {
}
}
"
`;

exports[`convert-host-binding generator should convert properly for component with duplicate host binding 1`] = `
"
import { Component } from '@angular/core';
@Component({
selector: 'my-duplicated-component',
template: 'My duplicated component',
host: { '[class.active]': 'isActive2' }
})
export class MyDuplicatedComponent {
isActive = true;
isActive2 = true;
"
`;
exports[`convert-host-binding generator should convert properly for component with host property 1`] = `
"
import { Component } from '@angular/core';
@Component({
selector: 'my-component',
template: 'My component',
host: {
'[class.active]': 'isActive',
'[attr.aria-disabled]': 'isDisabled'
}
})
export class MyComponent {
isActive = true;
get isDisabled() {
return true;
}
}
"
`;
exports[`convert-host-binding generator should convert properly for component with no host binding 1`] = `
"
import { Component } from '@angular/core';
@Component({
selector: 'my-empty-component',
template: 'My empty component',
})
export class MyEmptyComponent {
}
"
`;
exports[`convert-host-binding generator should convert properly for directive 1`] = `
"
import { Directive } from '@angular/core';
@Directive({
selector: '[myDirective]',
host: { '[class.active]': 'isActive',
'(keydown)': 'updateValue($event)'
}
})
export class MyDirective {
isActive = true;
updateValue(event: KeyboardEvent) {
}
}
"
`;
4 changes: 4 additions & 0 deletions libs/plugin/src/generators/convert-host-binding/compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { convertNxGenerator } from '@nx/devkit';
import { convertHostBindingGenerator } from './generator';

export default convertNxGenerator(convertHostBindingGenerator);
Loading

0 comments on commit d69d0f5

Please # to comment.