Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add host binding migration #528

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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']
---
Comment on lines +1 to +7
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be updated


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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorators are not yet deprecated


### 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 = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see some more test cases. And before we merge this one, we will need to make sure that we don't break apps.

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
Loading