-
Notifications
You must be signed in to change notification settings - Fork 90
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
``` |
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) { | ||
} | ||
} | ||
" | ||
`; |
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); |
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 = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
}); | ||
}); |
There was a problem hiding this comment.
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