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 Dec 3, 2024
1 parent 76d6138 commit 24f0916
Show file tree
Hide file tree
Showing 8 changed files with 533 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: Queries Migration
description: Schematics for migrating from decorator-based Queries to Signal-based Queries
entryPoint: plugin/src/generators/convert-queries
badge: stable
contributors: ['enea-jahollari']
---

Recent releases of Angular have deprecated the `@HostBinding` and `@HostListener` decorators, replacing them with `host` defined properties. This migration schematic will help you convert your existing `@HostBinding` and `@HostListener` decorators to the new `host` properties.

### 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,66 @@
// 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 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 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);
108 changes: 108 additions & 0 deletions libs/plugin/src/generators/convert-host-binding/generator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';

import { convertHostBindingGenerator } from './generator';
import { ConvertHostBindingGeneratorSchema } from './schema';

const filesMap = {
componentWithHostBinding: `
import { Component, HostBinding } from '@angular/core';
@Component({
selector: 'my-component',
template: 'My component',
})
export class MyComponent {
@HostBinding('class.active') isActive = true;
@HostBinding('attr.aria-disabled') get isDisabled() {
return true;
}
@HostBinding('tabIndex') getTabIndex() {
return this.isDisabled ? -1 : 0;
}
}
`,
directiveWithHostBinding: `
import { Directive, HostBinding } from '@angular/core';
@Directive({
selector: '[myDirective]'
})
export class MyDirective {
@HostBinding('class.active') isActive = true;
@HostListener('keydown', ['$event'])
updateValue(event: KeyboardEvent) {
}
}
`,

componentWithHostProperty: `
import { Component } from '@angular/core';
@Component({
selector: 'my-component',
template: 'My component',
host: {
'[class.active]': 'isActive',
}
})
export class MyComponent {
isActive = true;
@HostBinding('attr.aria-disabled') get isDisabled() {
return true;
}
}
`,
};

describe('convert-host-binding generator', () => {
let tree: Tree;
const options: ConvertHostBindingGeneratorSchema = {
path: 'libs/my-file.ts',
};

function setup(file: keyof typeof filesMap) {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
tree.write('package.json', `{"dependencies": {"@angular/core": "17.3.0"}}`);
tree.write(`libs/my-file.ts`, filesMap[file]);

return () => {
return [tree.read('libs/my-file.ts', 'utf8'), filesMap[file]];
};
}

beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});

it('should convert properly for component', async () => {
const readContent = setup('componentWithHostBinding');
await convertHostBindingGenerator(tree, options);

const [updated] = readContent();

expect(updated).toMatchSnapshot();
});

it('should convert properly for directive', async () => {
const readContent = setup('directiveWithHostBinding');
await convertHostBindingGenerator(tree, options);

const [updated] = readContent();

expect(updated).toMatchSnapshot();
});

it('should convert properly for component with host property', async () => {
const readContent = setup('componentWithHostProperty');
await convertHostBindingGenerator(tree, options);

const [updated] = readContent();

expect(updated).toMatchSnapshot();
});
});
Loading

0 comments on commit 24f0916

Please sign in to comment.