Skip to content

Commit 61c815e

Browse files
authored
output migration (#2911)
### Fixes # <!-- Mention the issues this PR addresses --> ### Checks - [ ] Ran `yarn test-build` - [ ] Updated relevant documentations - [ ] Updated matching config options in altair-static ### Changes proposed in this pull request: <!-- Describe the changes being introduced in this PR --> ## Summary by Sourcery Refactor all Angular components to use the new `output()` signal helper for outputs instead of `@Output()` EventEmitter, replacing `.next()` calls with `.emit()` and updating corresponding template bindings. Enhancements: - Migrate component outputs from EventEmitter to readonly signal-based outputs using the `output()` helper. - Replace all `.next()` invocations with `.emit()` and update event bindings in templates to use `.emit()`. - Remove obsolete imports of `Output` and `EventEmitter` across multiple components. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Refactor - Migrated many components/directives from EventEmitter/@output to Angular signal-based outputs; templates updated from next(...) to emit(...). Minor public API signature tweaks and one action payload type refined. - Documentation - Updated Angular component guidelines to recommend input/output helpers. - Style - Disabled standalone-component lint rule in ESLint. - Bug Fixes - History dialog now closes automatically after restoring an entry. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent d9a4bf3 commit 61c815e

File tree

69 files changed

+510
-464
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+510
-464
lines changed

.github/instructions/angular-components.instructions.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,51 +9,55 @@ These instructions guide the development of Angular components in the Altair Gra
99
## Component Structure and Conventions
1010

1111
### File Naming and Organization
12+
1213
- Components are located in `packages/altair-app/src/app/modules/altair/components/`
1314
- Follow kebab-case naming convention for component files (e.g., `query-editor.component.ts`)
1415
- Each component should have its own directory with related files (.ts, .html, .scss, .spec.ts)
1516

1617
### Component Class Structure
18+
1719
- Use OnPush change detection strategy for better performance when possible
1820
- Implement lifecycle hooks appropriately (OnInit, OnDestroy, etc.)
1921
- Follow the single responsibility principle - each component should have one clear purpose
2022
- Use dependency injection for services and dependencies
2123

2224
### State Management
25+
2326
- Use reactive programming patterns with RxJS observables
2427
- Properly manage subscriptions and clean them up in ngOnDestroy
2528
- Emit events using Angular's EventEmitter for parent-child communication
26-
- Use Input() and Output() decorators for component API
29+
- Use input and output for component API
2730

2831
### Example Component Pattern:
32+
2933
```typescript
3034
@Component({
3135
selector: 'app-example',
3236
templateUrl: './example.component.html',
3337
styleUrls: ['./example.component.scss'],
34-
changeDetection: ChangeDetectionStrategy.OnPush
38+
changeDetection: ChangeDetectionStrategy.OnPush,
3539
})
3640
export class ExampleComponent implements OnInit, OnDestroy {
37-
@Input() inputProperty: string = '';
38-
@Output() someEvent = new EventEmitter<SomeType>();
39-
41+
readonly inputProperty = input<string>('');
42+
readonly someEvent = output<SomeType>();
43+
4044
private subscription: Subscription;
41-
45+
4246
constructor(
4347
private store: Store<RootState>,
4448
private someService: SomeService
4549
) {}
46-
50+
4751
ngOnInit() {
48-
this.subscription = this.store.select(selectSomeState).subscribe(state => {
52+
this.subscription = this.store.select(selectSomeState).subscribe((state) => {
4953
// Handle state changes
5054
});
5155
}
52-
56+
5357
ngOnDestroy() {
5458
this.subscription?.unsubscribe();
5559
}
56-
60+
5761
onSomeAction() {
5862
this.someEvent.emit(someData);
5963
}
@@ -63,30 +67,35 @@ export class ExampleComponent implements OnInit, OnDestroy {
6367
## UI Library Integration
6468

6569
### ng-zorro Components
70+
6671
- Use ng-zorro ant design components for consistent UI
6772
- Follow existing patterns for modal dialogs, forms, and navigation
6873
- Don't test ng-zorro component properties in unit tests - focus on business logic
6974

7075
### Form Handling
76+
7177
- Use Angular reactive forms for complex forms
7278
- Implement proper validation and error handling
7379
- Follow accessibility guidelines
7480

7581
## Testing Guidelines
82+
7683
- Write unit tests focusing on component business logic only
7784
- Use the custom testing framework in `packages/altair-app/src/testing`
7885
- Test event emissions, state changes, and method behavior
7986
- Mock external dependencies and services
8087
- Don't test UI library components or template rendering details
8188

8289
## Performance Considerations
90+
8391
- Use OnPush change detection where appropriate
8492
- Implement trackBy functions for ngFor loops
8593
- Lazy load components when possible
8694
- Avoid memory leaks by properly unsubscribing from observables
8795

8896
## Accessibility
97+
8998
- Include proper ARIA attributes for screen readers
9099
- Ensure keyboard navigation works correctly
91100
- Use semantic HTML elements
92-
- Test with accessibility tools
101+
- Test with accessibility tools

packages/altair-app/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ module.exports = {
4040
],
4141
'@angular-eslint/prefer-on-push-component-change-detection': 'warn',
4242
// '@angular-eslint/prefer-standalone-component': 'warn',
43-
'@angular-eslint/prefer-standalone': 'warn',
43+
'@angular-eslint/prefer-standalone': 'off',
4444
'@angular-eslint/consistent-component-styles': 'off',
4545
'@angular-eslint/sort-keys-in-type-decorator': 'off',
4646
'@angular-eslint/use-injectable-provided-in': 'warn',

packages/altair-app/src/app/modules/altair/components/account-dialog/account-dialog.component.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import {
22
ChangeDetectionStrategy,
33
Component,
4-
EventEmitter,
5-
Output,
64
input,
5+
output
76
} from '@angular/core';
87
import { AccountState } from 'altair-graphql-core/build/types/state/account.interfaces';
98
import { apiClient } from '../../services/api/api.service';
@@ -20,9 +19,9 @@ import { IdentityProvider } from '@altairgraphql/db';
2019
export class AccountDialogComponent {
2120
readonly showDialog = input(true);
2221
readonly account = input<AccountState>();
23-
@Output() toggleDialogChange = new EventEmitter<boolean>();
24-
@Output() handleLoginChange = new EventEmitter<IdentityProvider>();
25-
@Output() logoutChange = new EventEmitter();
22+
readonly toggleDialogChange = output<boolean>();
23+
readonly handleLoginChange = output<IdentityProvider>();
24+
readonly logoutChange = output();
2625

2726
submitLogin(provider: IdentityProvider = IdentityProvider.GOOGLE) {
2827
this.handleLoginChange.emit(provider);
Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import {
22
Component,
3-
Output,
4-
EventEmitter,
53
ChangeDetectionStrategy,
6-
input
4+
input,
5+
output
76
} from '@angular/core';
87

98
@Component({
@@ -16,12 +15,12 @@ import {
1615
export class ActionBarComponent {
1716
readonly showDocs = input(false);
1817
readonly isSubscribed = input(false);
19-
@Output() toggleHeaderDialog = new EventEmitter();
20-
@Output() toggleVariableDialog = new EventEmitter();
21-
@Output() toggleDocsChange = new EventEmitter();
22-
@Output() reloadDocsChange = new EventEmitter();
23-
@Output() prettifyCodeChange = new EventEmitter();
24-
@Output() sendRequest = new EventEmitter();
25-
@Output() clearEditorChange = new EventEmitter();
26-
@Output() toggleSubscriptionUrlDialog = new EventEmitter();
18+
readonly toggleHeaderDialog = output();
19+
readonly toggleVariableDialog = output();
20+
readonly toggleDocsChange = output();
21+
readonly reloadDocsChange = output();
22+
readonly prettifyCodeChange = output();
23+
readonly sendRequest = output();
24+
readonly clearEditorChange = output();
25+
readonly toggleSubscriptionUrlDialog = output();
2726
}

packages/altair-app/src/app/modules/altair/components/add-collection-query-dialog/add-collection-query-dialog.component.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { Component, Output, EventEmitter, input, effect, ChangeDetectionStrategy, signal, computed, inject } from '@angular/core';
1+
import {
2+
Component,
3+
input,
4+
effect,
5+
ChangeDetectionStrategy,
6+
signal,
7+
computed,
8+
inject,
9+
output,
10+
} from '@angular/core';
211
import { WORKSPACES } from 'altair-graphql-core/build/types/state/workspace.interface';
312
import {
413
IQueryCollection,
@@ -24,18 +33,18 @@ export class AddCollectionQueryDialogComponent {
2433
readonly loggedIn = input(false);
2534
readonly workspaces = input<WorkspaceOption[]>([]);
2635

27-
@Output() toggleDialogChange = new EventEmitter();
28-
@Output() createCollectionAndSaveQueryToCollectionChange = new EventEmitter<{
36+
readonly toggleDialogChange = output<boolean>();
37+
readonly createCollectionAndSaveQueryToCollectionChange = output<{
2938
queryName: string;
3039
collectionName: string;
3140
parentCollectionId: string;
3241
workspaceId: string;
3342
}>();
34-
@Output() saveQueryToCollectionChange = new EventEmitter<{
43+
readonly saveQueryToCollectionChange = output<{
3544
queryName: string;
3645
collectionId: string;
3746
}>();
38-
@Output() newCollectionParentCollectionIdChange = new EventEmitter();
47+
readonly newCollectionParentCollectionIdChange = output();
3948

4049
get parentCollectionRootId() {
4150
return '0'; // 0 for root

packages/altair-app/src/app/modules/altair/components/authorization/authorization-apikey/authorization-apikey.component.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { ChangeDetectionStrategy, Component, OnInit, Output, input, inject } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
OnInit,
5+
input,
6+
inject,
7+
} from '@angular/core';
8+
import { outputFromObservable } from '@angular/core/rxjs-interop';
29
import { NonNullableFormBuilder } from '@angular/forms';
310
import { ApiKeyAuthorizationProviderInput } from 'altair-graphql-core/build/authorization/providers/api-key';
411

@@ -17,7 +24,7 @@ export class AuthorizationApikeyComponent implements OnInit {
1724
headerValue: '',
1825
});
1926
readonly authData = input<unknown>();
20-
@Output() authDataChange = this.apiKeyForm.valueChanges;
27+
readonly authDataChange = outputFromObservable(this.apiKeyForm.valueChanges);
2128

2229
ngOnInit(): void {
2330
const authData = this.authData();

packages/altair-app/src/app/modules/altair/components/authorization/authorization-basic/authorization-basic.component.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { ChangeDetectionStrategy, Component, OnInit, Output, input, inject } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
OnInit,
5+
input,
6+
inject,
7+
} from '@angular/core';
8+
import { outputFromObservable } from '@angular/core/rxjs-interop';
29
import { NonNullableFormBuilder } from '@angular/forms';
310

411
@Component({
@@ -16,7 +23,7 @@ export class AuthorizationBasicComponent implements OnInit {
1623
password: '',
1724
});
1825
readonly authData = input<unknown>();
19-
@Output() authDataChange = this.basicForm.valueChanges;
26+
readonly authDataChange = outputFromObservable(this.basicForm.valueChanges);
2027

2128
ngOnInit(): void {
2229
const authData = this.authData();

packages/altair-app/src/app/modules/altair/components/authorization/authorization-bearer/authorization-bearer.component.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { ChangeDetectionStrategy, Component, OnInit, Output, input, inject } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
OnInit,
5+
Output,
6+
input,
7+
inject,
8+
} from '@angular/core';
9+
import { outputFromObservable } from '@angular/core/rxjs-interop';
210
import { NonNullableFormBuilder } from '@angular/forms';
311

412
@Component({
@@ -15,7 +23,7 @@ export class AuthorizationBearerComponent implements OnInit {
1523
token: '',
1624
});
1725
readonly authData = input<unknown>();
18-
@Output() authDataChange = this.bearerForm.valueChanges;
26+
readonly authDataChange = outputFromObservable(this.bearerForm.valueChanges);
1927

2028
ngOnInit(): void {
2129
const authData = this.authData();

packages/altair-app/src/app/modules/altair/components/authorization/authorization-editor/authorization-editor.component.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output, input, inject } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
OnInit,
5+
input,
6+
inject,
7+
output,
8+
} from '@angular/core';
29
import { NonNullableFormBuilder } from '@angular/forms';
310
import {
4-
AUTHORIZATION_TYPES,
511
AUTHORIZATION_TYPE_LIST,
612
AuthorizationState,
713
AuthorizationTypes,
814
DEFAULT_AUTHORIZATION_TYPE,
915
} from 'altair-graphql-core/build/types/state/authorization.interface';
1016
import { distinctUntilChanged, map } from 'rxjs/operators';
1117
import { AUTHORIZATION_MAPPING } from '../authorizations';
18+
import { outputFromObservable } from '@angular/core/rxjs-interop';
1219

1320
@Component({
1421
selector: 'app-authorization-editor',
@@ -27,11 +34,13 @@ export class AuthorizationEditorComponent implements OnInit {
2734
});
2835

2936
readonly authorizationState = input<AuthorizationState>();
30-
@Output() authTypeChange = this.typeForm.valueChanges.pipe(
31-
map(({ type }) => type ?? DEFAULT_AUTHORIZATION_TYPE),
32-
distinctUntilChanged()
37+
readonly authTypeChange = outputFromObservable(
38+
this.typeForm.valueChanges.pipe(
39+
map(({ type }) => type ?? DEFAULT_AUTHORIZATION_TYPE),
40+
distinctUntilChanged()
41+
)
3342
);
34-
@Output() authDataChange = new EventEmitter();
43+
readonly authDataChange = output<unknown>();
3544
AUTH_MAPPING = AUTHORIZATION_MAPPING;
3645
AUTH_TYPES = AUTHORIZATION_TYPE_LIST;
3746

packages/altair-app/src/app/modules/altair/components/authorization/authorization-oauth2/authorization-oauth2.component.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { ChangeDetectionStrategy, Component, OnInit, Output, input, inject } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
OnInit,
5+
input,
6+
inject,
7+
} from '@angular/core';
8+
import { outputFromObservable } from '@angular/core/rxjs-interop';
29
import { NonNullableFormBuilder } from '@angular/forms';
310
import { getAltairConfig } from 'altair-graphql-core/build/config';
411
import {
@@ -54,7 +61,7 @@ export class AuthorizationOauth2Component implements OnInit {
5461
requestFormat = RequestFormat;
5562

5663
readonly authData = input<unknown>();
57-
@Output() authDataChange = this.form.valueChanges;
64+
readonly authDataChange = outputFromObservable(this.form.valueChanges);
5865

5966
optionsShape = this.getOptionsShape();
6067

0 commit comments

Comments
 (0)