diff --git a/.all-contributorsrc b/.all-contributorsrc
index cc7f5ba4..c60fd019 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -457,6 +457,15 @@
"code",
"bug"
]
+ },
+ {
+ "login": "jdegand",
+ "name": "J. Degand",
+ "avatar_url": "https://avatars.githubusercontent.com/u/70610011?v=4",
+ "profile": "https://github.com/jdegand",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index d9d16a33..848aed06 100644
--- a/README.md
+++ b/README.md
@@ -278,6 +278,7 @@ Thanks goes to these people ([emoji key][emojis]):
 Jamie Vereecken 💻 |
 Christian24 💻 👀 |
 Michal Štrajt 💻 🐛 |
+  J. Degand 💻 |
diff --git a/apps/example-app-karma/eslint.config.mjs b/apps/example-app-karma/eslint.config.mjs
index 8f627dbf..bd9b42bf 100644
--- a/apps/example-app-karma/eslint.config.mjs
+++ b/apps/example-app-karma/eslint.config.mjs
@@ -1,8 +1,6 @@
// @ts-check
-import tseslint from "typescript-eslint";
-import rootConfig from "../../eslint.config.mjs";
+import tseslint from 'typescript-eslint';
+import rootConfig from '../../eslint.config.mjs';
-export default tseslint.config(
- ...rootConfig,
-);
+export default tseslint.config(...rootConfig);
diff --git a/apps/example-app-karma/src/app/examples/login-form.spec.ts b/apps/example-app-karma/src/app/examples/login-form.spec.ts
index a0282341..d019e069 100644
--- a/apps/example-app-karma/src/app/examples/login-form.spec.ts
+++ b/apps/example-app-karma/src/app/examples/login-form.spec.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/angular';
@@ -45,13 +45,13 @@ it('should display invalid message and submit button must be disabled', async ()
`,
})
class LoginComponent {
+ private fb = inject(FormBuilder);
+
form: FormGroup = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
- constructor(private fb: FormBuilder) {}
-
get email(): FormControl {
return this.form.get('email') as FormControl;
}
diff --git a/apps/example-app-karma/src/app/issues/issue-491.spec.ts b/apps/example-app-karma/src/app/issues/issue-491.spec.ts
index 9320251e..9c967710 100644
--- a/apps/example-app-karma/src/app/issues/issue-491.spec.ts
+++ b/apps/example-app-karma/src/app/issues/issue-491.spec.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { render, screen } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
@@ -43,7 +43,7 @@ it('test click event with router.navigate', async () => {
`,
})
class LoginComponent {
- constructor(private router: Router) {}
+ private readonly router = inject(Router);
onSubmit(): void {
this.router.navigate(['logged-in']);
}
diff --git a/apps/example-app/eslint.config.mjs b/apps/example-app/eslint.config.mjs
index 01625848..bd9b42bf 100644
--- a/apps/example-app/eslint.config.mjs
+++ b/apps/example-app/eslint.config.mjs
@@ -1,8 +1,6 @@
// @ts-check
-import tseslint from "typescript-eslint";
-import rootConfig from "../../eslint.config.mjs";
+import tseslint from 'typescript-eslint';
+import rootConfig from '../../eslint.config.mjs';
-export default tseslint.config(
- ...rootConfig,
-);
\ No newline at end of file
+export default tseslint.config(...rootConfig);
diff --git a/apps/example-app/src/app/examples/03-forms.ts b/apps/example-app/src/app/examples/03-forms.ts
index a62d8650..c1e48c23 100644
--- a/apps/example-app/src/app/examples/03-forms.ts
+++ b/apps/example-app/src/app/examples/03-forms.ts
@@ -1,5 +1,5 @@
import { NgForOf, NgIf } from '@angular/common';
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
@@ -33,6 +33,8 @@ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
`,
})
export class FormsComponent {
+ private formBuilder = inject(FormBuilder);
+
colors = [
{ id: 'R', value: 'Red' },
{ id: 'B', value: 'Blue' },
@@ -45,8 +47,6 @@ export class FormsComponent {
color: [null as string | null, Validators.required],
});
- constructor(private formBuilder: FormBuilder) {}
-
get formErrors() {
return Object.keys(this.form.controls)
.map((formKey) => {
diff --git a/apps/example-app/src/app/examples/04-forms-with-material.ts b/apps/example-app/src/app/examples/04-forms-with-material.ts
index cf117a51..2376c725 100644
--- a/apps/example-app/src/app/examples/04-forms-with-material.ts
+++ b/apps/example-app/src/app/examples/04-forms-with-material.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { NgForOf, NgIf } from '@angular/common';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -84,6 +84,8 @@ import { MatNativeDateModule } from '@angular/material/core';
],
})
export class MaterialFormsComponent {
+ private formBuilder = inject(FormBuilder);
+
colors = [
{ id: 'R', value: 'Red' },
{ id: 'B', value: 'Blue' },
@@ -97,8 +99,6 @@ export class MaterialFormsComponent {
agree: [false, Validators.requiredTrue],
});
- constructor(private formBuilder: FormBuilder) {}
-
get colorControlDisplayValue(): string | undefined {
const selectedId = this.form.get('color')?.value;
return this.colors.filter((color) => color.id === selectedId)[0]?.value;
diff --git a/apps/example-app/src/app/examples/05-component-provider.ts b/apps/example-app/src/app/examples/05-component-provider.ts
index 2d66b070..c6162e0b 100644
--- a/apps/example-app/src/app/examples/05-component-provider.ts
+++ b/apps/example-app/src/app/examples/05-component-provider.ts
@@ -1,4 +1,4 @@
-import { Component, Injectable } from '@angular/core';
+import { Component, inject, Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
@@ -30,5 +30,5 @@ export class CounterService {
providers: [CounterService],
})
export class ComponentWithProviderComponent {
- constructor(public counter: CounterService) {}
+ protected counter = inject(CounterService);
}
diff --git a/apps/example-app/src/app/examples/06-with-ngrx-store.ts b/apps/example-app/src/app/examples/06-with-ngrx-store.ts
index 8702843c..f478e528 100644
--- a/apps/example-app/src/app/examples/06-with-ngrx-store.ts
+++ b/apps/example-app/src/app/examples/06-with-ngrx-store.ts
@@ -1,5 +1,5 @@
import { AsyncPipe } from '@angular/common';
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { createSelector, Store, createAction, createReducer, on, select } from '@ngrx/store';
const increment = createAction('increment');
@@ -26,8 +26,9 @@ const selectValue = createSelector(
`,
})
export class WithNgRxStoreComponent {
+ private store = inject(Store);
+
value = this.store.pipe(select(selectValue));
- constructor(private store: Store) {}
increment() {
this.store.dispatch(increment());
diff --git a/apps/example-app/src/app/examples/07-with-ngrx-mock-store.ts b/apps/example-app/src/app/examples/07-with-ngrx-mock-store.ts
index 915a88d8..0bd5d864 100644
--- a/apps/example-app/src/app/examples/07-with-ngrx-mock-store.ts
+++ b/apps/example-app/src/app/examples/07-with-ngrx-mock-store.ts
@@ -1,5 +1,5 @@
import { AsyncPipe, NgForOf } from '@angular/common';
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { createSelector, Store, select } from '@ngrx/store';
export const selectItems = createSelector(
@@ -20,8 +20,9 @@ export const selectItems = createSelector(
`,
})
export class WithNgRxMockStoreComponent {
+ private store = inject(Store);
+
items = this.store.pipe(select(selectItems));
- constructor(private store: Store) {}
send(item: string) {
this.store.dispatch({ type: '[Item List] send', item });
diff --git a/apps/example-app/src/app/examples/08-directive.ts b/apps/example-app/src/app/examples/08-directive.ts
index 63efe415..d6cd631c 100644
--- a/apps/example-app/src/app/examples/08-directive.ts
+++ b/apps/example-app/src/app/examples/08-directive.ts
@@ -1,15 +1,15 @@
-import { Directive, HostListener, ElementRef, Input, OnInit } from '@angular/core';
+import { Directive, HostListener, ElementRef, Input, OnInit, inject } from '@angular/core';
@Directive({
standalone: true,
selector: '[atlSpoiler]',
})
export class SpoilerDirective implements OnInit {
+ private el = inject(ElementRef);
+
@Input() hidden = 'SPOILER';
@Input() visible = 'I am visible now...';
- constructor(private el: ElementRef) {}
-
ngOnInit() {
this.el.nativeElement.textContent = this.hidden;
}
diff --git a/apps/example-app/src/app/examples/09-router.ts b/apps/example-app/src/app/examples/09-router.ts
index e46773be..f29a4efe 100644
--- a/apps/example-app/src/app/examples/09-router.ts
+++ b/apps/example-app/src/app/examples/09-router.ts
@@ -1,5 +1,5 @@
import { AsyncPipe } from '@angular/common';
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLink, RouterOutlet } from '@angular/router';
import { map } from 'rxjs/operators';
@@ -32,10 +32,10 @@ export class RootComponent {}
`,
})
export class DetailComponent {
+ private route = inject(ActivatedRoute);
id = this.route.paramMap.pipe(map((params) => params.get('id')));
text = this.route.queryParams.pipe(map((params) => params['text']));
subtext = this.route.queryParams.pipe(map((params) => params['subtext']));
- constructor(private route: ActivatedRoute) {}
}
@Component({
diff --git a/apps/example-app/src/app/examples/10-inject-token-dependency.ts b/apps/example-app/src/app/examples/10-inject-token-dependency.ts
index f7b2f661..5cd60498 100644
--- a/apps/example-app/src/app/examples/10-inject-token-dependency.ts
+++ b/apps/example-app/src/app/examples/10-inject-token-dependency.ts
@@ -1,4 +1,4 @@
-import { Component, InjectionToken, Inject } from '@angular/core';
+import { Component, InjectionToken, inject } from '@angular/core';
export const DATA = new InjectionToken<{ text: string }>('Components Data');
@@ -8,5 +8,5 @@ export const DATA = new InjectionToken<{ text: string }>('Components Data');
template: ' {{ data.text }} ',
})
export class DataInjectedComponent {
- constructor(@Inject(DATA) public data: { text: string }) {}
+ protected data = inject(DATA);
}
diff --git a/apps/example-app/src/app/examples/12-service-component.ts b/apps/example-app/src/app/examples/12-service-component.ts
index 1746eb2c..f1b848ba 100644
--- a/apps/example-app/src/app/examples/12-service-component.ts
+++ b/apps/example-app/src/app/examples/12-service-component.ts
@@ -1,5 +1,5 @@
import { AsyncPipe, NgForOf } from '@angular/common';
-import { Component, Injectable } from '@angular/core';
+import { Component, inject, Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
export class Customer {
@@ -29,6 +29,6 @@ export class CustomersService {
`,
})
export class CustomersComponent {
+ private service = inject(CustomersService);
customers$ = this.service.load();
- constructor(private service: CustomersService) {}
}
diff --git a/apps/example-app/src/app/examples/15-dialog.component.spec.ts b/apps/example-app/src/app/examples/15-dialog.component.spec.ts
index df172be8..51f8fb04 100644
--- a/apps/example-app/src/app/examples/15-dialog.component.spec.ts
+++ b/apps/example-app/src/app/examples/15-dialog.component.spec.ts
@@ -1,5 +1,5 @@
import { MatDialogRef } from '@angular/material/dialog';
-import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { provideNoopAnimations } from '@angular/platform-browser/animations';
import { render, screen } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
@@ -10,8 +10,8 @@ test('dialog closes', async () => {
const closeFn = jest.fn();
await render(DialogContentComponent, {
- imports: [NoopAnimationsModule],
providers: [
+ provideNoopAnimations(),
{
provide: MatDialogRef,
useValue: {
@@ -31,7 +31,7 @@ test('closes the dialog via the backdrop', async () => {
const user = userEvent.setup();
await render(DialogComponent, {
- imports: [NoopAnimationsModule],
+ providers: [provideNoopAnimations()],
});
const openDialogButton = await screen.findByRole('button', { name: /open dialog/i });
@@ -55,7 +55,7 @@ test('opens and closes the dialog with buttons', async () => {
const user = userEvent.setup();
await render(DialogComponent, {
- imports: [NoopAnimationsModule],
+ providers: [provideNoopAnimations()],
});
const openDialogButton = await screen.findByRole('button', { name: /open dialog/i });
diff --git a/apps/example-app/src/app/examples/15-dialog.component.ts b/apps/example-app/src/app/examples/15-dialog.component.ts
index 029ee64e..ce951f23 100644
--- a/apps/example-app/src/app/examples/15-dialog.component.ts
+++ b/apps/example-app/src/app/examples/15-dialog.component.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
@Component({
@@ -8,7 +8,7 @@ import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dial
template: '',
})
export class DialogComponent {
- constructor(public dialog: MatDialog) {}
+ private dialog = inject(MatDialog);
openDialog(): void {
this.dialog.open(DialogContentComponent);
@@ -29,7 +29,7 @@ export class DialogComponent {
`,
})
export class DialogContentComponent {
- constructor(public dialogRef: MatDialogRef) {}
+ private dialogRef = inject>(MatDialogRef);
cancel(): void {
this.dialogRef.close();
diff --git a/apps/example-app/src/app/examples/16-input-getter-setter.ts b/apps/example-app/src/app/examples/16-input-getter-setter.ts
index 4c189000..9d0654d3 100644
--- a/apps/example-app/src/app/examples/16-input-getter-setter.ts
+++ b/apps/example-app/src/app/examples/16-input-getter-setter.ts
@@ -8,7 +8,6 @@ import { Component, Input } from '@angular/core';
{{ value }}
`,
})
-// eslint-disable-next-line @angular-eslint/component-class-suffix
export class InputGetterSetter {
@Input() set value(value: string) {
this.originalValue = value;
diff --git a/apps/example-app/src/app/examples/20-test-harness.ts b/apps/example-app/src/app/examples/20-test-harness.ts
index 8e5e4071..0ecb7b35 100644
--- a/apps/example-app/src/app/examples/20-test-harness.ts
+++ b/apps/example-app/src/app/examples/20-test-harness.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
@@ -11,7 +11,7 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
`,
})
export class HarnessComponent {
- constructor(private snackBar: MatSnackBar) {}
+ private snackBar = inject(MatSnackBar);
openSnackBar() {
return this.snackBar.open('Pizza Party!!!');
diff --git a/apps/example-app/src/app/examples/23-host-directive.ts b/apps/example-app/src/app/examples/23-host-directive.ts
index 3e201c75..3d27f788 100644
--- a/apps/example-app/src/app/examples/23-host-directive.ts
+++ b/apps/example-app/src/app/examples/23-host-directive.ts
@@ -1,13 +1,12 @@
-import { Component, Directive, ElementRef, input, OnInit } from '@angular/core';
+import { Component, Directive, ElementRef, inject, input, OnInit } from '@angular/core';
@Directive({
selector: '[atlText]',
})
export class TextDirective implements OnInit {
+ private el = inject(ElementRef);
atlText = input('');
- constructor(private el: ElementRef) {}
-
ngOnInit() {
this.el.nativeElement.textContent = this.atlText();
}
diff --git a/apps/example-app/src/app/examples/24-bindings-api.component.spec.ts b/apps/example-app/src/app/examples/24-bindings-api.component.spec.ts
new file mode 100644
index 00000000..6c0a0e32
--- /dev/null
+++ b/apps/example-app/src/app/examples/24-bindings-api.component.spec.ts
@@ -0,0 +1,147 @@
+import { signal, inputBinding, outputBinding, twoWayBinding } from '@angular/core';
+import { render, screen } from '@testing-library/angular';
+import { BindingsApiExampleComponent } from './24-bindings-api.component';
+
+test('displays computed greeting message with input values', async () => {
+ await render(BindingsApiExampleComponent, {
+ bindings: [
+ inputBinding('greeting', () => 'Hello'),
+ inputBinding('age', () => 25),
+ twoWayBinding('name', signal('John')),
+ ],
+ });
+
+ expect(screen.getByTestId('input-value')).toHaveTextContent('Hello John of 25 years old');
+ expect(screen.getByTestId('computed-value')).toHaveTextContent('Hello John of 25 years old');
+ expect(screen.getByTestId('current-age')).toHaveTextContent('Current age: 25');
+});
+
+test('emits submitValue output when submit button is clicked', async () => {
+ const submitHandler = jest.fn();
+ const nameSignal = signal('Alice');
+
+ await render(BindingsApiExampleComponent, {
+ bindings: [
+ inputBinding('greeting', () => 'Good morning'),
+ inputBinding('age', () => 28),
+ twoWayBinding('name', nameSignal),
+ outputBinding('submitValue', submitHandler),
+ ],
+ });
+
+ const submitButton = screen.getByTestId('submit-button');
+ submitButton.click();
+ expect(submitHandler).toHaveBeenCalledWith('Alice');
+});
+
+test('emits ageChanged output when increment button is clicked', async () => {
+ const ageChangedHandler = jest.fn();
+
+ await render(BindingsApiExampleComponent, {
+ bindings: [
+ inputBinding('greeting', () => 'Hi'),
+ inputBinding('age', () => 20),
+ twoWayBinding('name', signal('Charlie')),
+ outputBinding('ageChanged', ageChangedHandler),
+ ],
+ });
+
+ const incrementButton = screen.getByTestId('increment-button');
+ incrementButton.click();
+
+ expect(ageChangedHandler).toHaveBeenCalledWith(21);
+});
+
+test('updates name through two-way binding when input changes', async () => {
+ const nameSignal = signal('Initial Name');
+
+ await render(BindingsApiExampleComponent, {
+ bindings: [
+ inputBinding('greeting', () => 'Hello'),
+ inputBinding('age', () => 25),
+ twoWayBinding('name', nameSignal),
+ ],
+ });
+
+ const nameInput = screen.getByTestId('name-input') as HTMLInputElement;
+
+ // Verify initial value
+ expect(nameInput.value).toBe('Initial Name');
+ expect(screen.getByTestId('input-value')).toHaveTextContent('Hello Initial Name of 25 years old');
+
+ // Update the signal externally
+ nameSignal.set('Updated Name');
+
+ // Verify the input and display update
+ expect(await screen.findByDisplayValue('Updated Name')).toBeInTheDocument();
+ expect(screen.getByTestId('input-value')).toHaveTextContent('Hello Updated Name of 25 years old');
+ expect(screen.getByTestId('computed-value')).toHaveTextContent('Hello Updated Name of 25 years old');
+});
+
+test('updates computed value when inputs change', async () => {
+ const greetingSignal = signal('Good day');
+ const nameSignal = signal('David');
+ const ageSignal = signal(35);
+
+ const { fixture } = await render(BindingsApiExampleComponent, {
+ bindings: [
+ inputBinding('greeting', greetingSignal),
+ inputBinding('age', ageSignal),
+ twoWayBinding('name', nameSignal),
+ ],
+ });
+
+ // Initial state
+ expect(screen.getByTestId('computed-value')).toHaveTextContent('Good day David of 35 years old');
+
+ // Update greeting
+ greetingSignal.set('Good evening');
+ fixture.detectChanges();
+ expect(screen.getByTestId('computed-value')).toHaveTextContent('Good evening David of 35 years old');
+
+ // Update age
+ ageSignal.set(36);
+ fixture.detectChanges();
+ expect(screen.getByTestId('computed-value')).toHaveTextContent('Good evening David of 36 years old');
+
+ // Update name
+ nameSignal.set('Daniel');
+ fixture.detectChanges();
+ expect(screen.getByTestId('computed-value')).toHaveTextContent('Good evening Daniel of 36 years old');
+});
+
+test('handles multiple output emissions correctly', async () => {
+ const submitHandler = jest.fn();
+ const ageChangedHandler = jest.fn();
+ const nameSignal = signal('Emma');
+
+ await render(BindingsApiExampleComponent, {
+ bindings: [
+ inputBinding('greeting', () => 'Hey'),
+ inputBinding('age', () => 22),
+ twoWayBinding('name', nameSignal),
+ outputBinding('submitValue', submitHandler),
+ outputBinding('ageChanged', ageChangedHandler),
+ ],
+ });
+
+ // Click submit button multiple times
+ const submitButton = screen.getByTestId('submit-button');
+ submitButton.click();
+ submitButton.click();
+
+ expect(submitHandler).toHaveBeenCalledTimes(2);
+ expect(submitHandler).toHaveBeenNthCalledWith(1, 'Emma');
+ expect(submitHandler).toHaveBeenNthCalledWith(2, 'Emma');
+
+ // Click increment button multiple times
+ const incrementButton = screen.getByTestId('increment-button');
+ incrementButton.click();
+ incrementButton.click();
+ incrementButton.click();
+
+ expect(ageChangedHandler).toHaveBeenCalledTimes(3);
+ expect(ageChangedHandler).toHaveBeenNthCalledWith(1, 23);
+ expect(ageChangedHandler).toHaveBeenNthCalledWith(2, 23); // Still 23 because age input doesn't change
+ expect(ageChangedHandler).toHaveBeenNthCalledWith(3, 23);
+});
diff --git a/apps/example-app/src/app/examples/24-bindings-api.component.ts b/apps/example-app/src/app/examples/24-bindings-api.component.ts
new file mode 100644
index 00000000..eb61ebeb
--- /dev/null
+++ b/apps/example-app/src/app/examples/24-bindings-api.component.ts
@@ -0,0 +1,36 @@
+import { Component, computed, input, model, numberAttribute, output } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+@Component({
+ selector: 'atl-bindings-api-example',
+ template: `
+ {{ greetings() }} {{ name() }} of {{ age() }} years old
+ {{ greetingMessage() }}
+
+
+
+ Current age: {{ age() }}
+ `,
+ standalone: true,
+ imports: [FormsModule],
+})
+export class BindingsApiExampleComponent {
+ greetings = input('', {
+ alias: 'greeting',
+ });
+ age = input.required({ transform: numberAttribute });
+ name = model.required();
+ submitValue = output();
+ ageChanged = output();
+
+ greetingMessage = computed(() => `${this.greetings()} ${this.name()} of ${this.age()} years old`);
+
+ submitName() {
+ this.submitValue.emit(this.name());
+ }
+
+ incrementAge() {
+ const newAge = this.age() + 1;
+ this.ageChanged.emit(newAge);
+ }
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 95e031ac..18ef575e 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,14 +1,14 @@
// @ts-check
-import eslint from "@eslint/js";
-import tseslint from "typescript-eslint";
-import angular from "angular-eslint";
+import eslint from '@eslint/js';
+import tseslint from 'typescript-eslint';
+import angular from 'angular-eslint';
import jestDom from 'eslint-plugin-jest-dom';
import testingLibrary from 'eslint-plugin-testing-library';
export default tseslint.config(
{
- files: ["**/*.ts"],
+ files: ['**/*.ts'],
extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
@@ -17,50 +17,42 @@ export default tseslint.config(
],
processor: angular.processInlineTemplates,
rules: {
- "@angular-eslint/directive-selector": [
- "error",
+ '@angular-eslint/directive-selector': [
+ 'error',
{
- type: "attribute",
- prefix: "atl",
- style: "camelCase",
+ type: 'attribute',
+ prefix: 'atl',
+ style: 'camelCase',
},
],
- "@angular-eslint/component-selector": [
- "error",
+ '@angular-eslint/component-selector': [
+ 'error',
{
- type: "element",
- prefix: "atl",
- style: "kebab-case",
+ type: 'element',
+ prefix: 'atl',
+ style: 'kebab-case',
},
],
- "@typescript-eslint/no-explicit-any": "off",
- "@typescript-eslint/no-unused-vars": [
- "error",
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
{
- "argsIgnorePattern": "^_",
- "varsIgnorePattern": "^_",
- "caughtErrorsIgnorePattern": "^_"
- }
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ caughtErrorsIgnorePattern: '^_',
+ },
],
- // These are needed for test cases
"@angular-eslint/prefer-standalone": "off",
"@angular-eslint/no-input-rename": "off",
- "@angular-eslint/no-input-rename": "off",
},
},
{
- files: ["**/*.spec.ts"],
- extends: [
- jestDom.configs["flat/recommended"],
- testingLibrary.configs["flat/angular"],
- ],
- },
+ files: ['**/*.spec.ts'],
+ extends: [jestDom.configs['flat/recommended'], testingLibrary.configs['flat/angular']],
+ },
{
- files: ["**/*.html"],
- extends: [
- ...angular.configs.templateRecommended,
- ...angular.configs.templateAccessibility,
- ],
+ files: ['**/*.html'],
+ extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
rules: {},
- }
+ },
);
diff --git a/package.json b/package.json
index 38883955..341eb0f5 100644
--- a/package.json
+++ b/package.json
@@ -27,53 +27,53 @@
"prepare": "git config core.hookspath .githooks"
},
"dependencies": {
- "@angular/animations": "20.0.0",
- "@angular/cdk": "20.0.0",
- "@angular/common": "20.0.0",
- "@angular/compiler": "20.0.0",
- "@angular/core": "20.0.0",
- "@angular/material": "20.0.0",
- "@angular/platform-browser": "20.0.0",
- "@angular/platform-browser-dynamic": "20.0.0",
- "@angular/router": "20.0.0",
- "@ngrx/store": "19.0.0",
- "@nx/angular": "21.1.2",
+ "@angular/animations": "20.1.7",
+ "@angular/cdk": "20.1.6",
+ "@angular/common": "20.1.7",
+ "@angular/compiler": "20.1.7",
+ "@angular/core": "20.1.7",
+ "@angular/material": "20.1.6",
+ "@angular/platform-browser": "20.1.7",
+ "@angular/platform-browser-dynamic": "20.1.7",
+ "@angular/router": "20.1.7",
+ "@ngrx/store": "20.0.0",
+ "@nx/angular": "21.3.11",
"@testing-library/dom": "^10.4.0",
"rxjs": "7.8.0",
"tslib": "~2.8.1",
"zone.js": "^0.15.0"
},
"devDependencies": {
- "@angular-devkit/build-angular": "20.0.0",
- "@angular-devkit/core": "20.0.0",
- "@angular-devkit/schematics": "20.0.0",
- "@angular-eslint/builder": "19.2.0",
- "@angular-eslint/eslint-plugin": "19.2.0",
- "@angular-eslint/eslint-plugin-template": "19.2.0",
- "@angular-eslint/schematics": "19.2.0",
- "@angular-eslint/template-parser": "19.2.0",
+ "@angular-devkit/build-angular": "20.1.6",
+ "@angular-devkit/core": "20.1.6",
+ "@angular-devkit/schematics": "20.1.6",
+ "@angular-eslint/builder": "20.0.0",
+ "@angular-eslint/eslint-plugin": "20.0.0",
+ "@angular-eslint/eslint-plugin-template": "20.0.0",
+ "@angular-eslint/schematics": "20.0.0",
+ "@angular-eslint/template-parser": "20.0.0",
"@angular/cli": "~20.0.0",
- "@angular/compiler-cli": "20.0.0",
- "@angular/forms": "20.0.0",
- "@angular/language-service": "20.0.0",
+ "@angular/compiler-cli": "20.1.7",
+ "@angular/forms": "20.1.7",
+ "@angular/language-service": "20.1.7",
"@eslint/eslintrc": "^2.1.1",
- "@nx/eslint": "21.1.2",
- "@nx/eslint-plugin": "21.1.2",
- "@nx/jest": "21.1.2",
- "@nx/node": "21.1.2",
- "@nx/plugin": "21.1.2",
- "@nx/workspace": "21.1.2",
- "@schematics/angular": "20.0.0",
+ "@nx/eslint": "21.3.11",
+ "@nx/eslint-plugin": "21.3.11",
+ "@nx/jest": "21.3.11",
+ "@nx/node": "21.3.11",
+ "@nx/plugin": "21.3.11",
+ "@nx/workspace": "21.3.11",
+ "@schematics/angular": "20.1.6",
"@testing-library/jasmine-dom": "^1.3.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.5.2",
"@types/jasmine": "4.3.1",
- "@types/jest": "29.5.14",
+ "@types/jest": "30.0.0",
"@types/node": "22.10.1",
"@types/testing-library__jasmine-dom": "^1.3.4",
"@typescript-eslint/types": "^8.19.0",
"@typescript-eslint/utils": "^8.19.0",
- "angular-eslint": "19.2.0",
+ "angular-eslint": "20.0.0",
"autoprefixer": "^10.4.20",
"cpy-cli": "^5.0.0",
"eslint": "^9.8.0",
@@ -81,9 +81,9 @@
"eslint-plugin-testing-library": "~7.1.1",
"jasmine-core": "4.2.0",
"jasmine-spec-reporter": "7.0.0",
- "jest": "29.7.0",
- "jest-environment-jsdom": "29.7.0",
- "jest-preset-angular": "14.4.2",
+ "jest": "30.0.5",
+ "jest-environment-jsdom": "30.0.5",
+ "jest-preset-angular": "15.0.0",
"karma": "6.4.0",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
@@ -91,8 +91,8 @@
"karma-jasmine-html-reporter": "2.0.0",
"lint-staged": "^15.3.0",
"ng-mocks": "^14.13.1",
- "ng-packagr": "20.0.0",
- "nx": "21.1.2",
+ "ng-packagr": "20.1.0",
+ "nx": "21.3.11",
"postcss": "^8.4.49",
"postcss-import": "14.1.0",
"postcss-preset-env": "7.5.0",
@@ -100,9 +100,10 @@
"prettier": "2.6.2",
"rimraf": "^5.0.10",
"semantic-release": "^24.2.1",
- "ts-jest": "29.1.0",
+ "ts-jest": "29.4.1",
"ts-node": "10.9.1",
"typescript": "5.8.2",
- "typescript-eslint": "^8.19.0"
+ "typescript-eslint": "^8.19.0",
+ "jest-util": "30.0.5"
}
}
diff --git a/projects/testing-library/eslint.config.mjs b/projects/testing-library/eslint.config.mjs
index 8f627dbf..bd9b42bf 100644
--- a/projects/testing-library/eslint.config.mjs
+++ b/projects/testing-library/eslint.config.mjs
@@ -1,8 +1,6 @@
// @ts-check
-import tseslint from "typescript-eslint";
-import rootConfig from "../../eslint.config.mjs";
+import tseslint from 'typescript-eslint';
+import rootConfig from '../../eslint.config.mjs';
-export default tseslint.config(
- ...rootConfig,
-);
+export default tseslint.config(...rootConfig);
diff --git a/projects/testing-library/jest-utils/tests/create-mock.spec.ts b/projects/testing-library/jest-utils/tests/create-mock.spec.ts
index 2393fe30..c20109b6 100644
--- a/projects/testing-library/jest-utils/tests/create-mock.spec.ts
+++ b/projects/testing-library/jest-utils/tests/create-mock.spec.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { fireEvent, render, screen } from '@testing-library/angular';
@@ -21,7 +21,7 @@ class FixtureService {
template: ` `,
})
class FixtureComponent {
- constructor(private service: FixtureService) {}
+ private service = inject(FixtureService);
print() {
this.service.print();
diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts
index 318bd2be..b8628bae 100644
--- a/projects/testing-library/src/lib/models.ts
+++ b/projects/testing-library/src/lib/models.ts
@@ -7,6 +7,7 @@ import {
Provider,
Signal,
InputSignalWithTransform,
+ Binding,
} from '@angular/core';
import { ComponentFixture, DeferBlockBehavior, DeferBlockState, TestBed } from '@angular/core/testing';
import { Routes } from '@angular/router';
@@ -307,6 +308,28 @@ export interface RenderComponentOptions;
+ /**
+ * @description
+ * An array of bindings to apply to the component using Angular's native bindings API.
+ * This provides a more direct way to bind inputs and outputs compared to the `inputs` and `on` options.
+ *
+ * @default
+ * []
+ *
+ * @example
+ * import { inputBinding, outputBinding, twoWayBinding } from '@angular/core';
+ * import { signal } from '@angular/core';
+ *
+ * await render(AppComponent, {
+ * bindings: [
+ * inputBinding('value', () => 'test value'),
+ * outputBinding('click', (event) => console.log(event)),
+ * twoWayBinding('name', signal('initial value'))
+ * ]
+ * })
+ */
+ bindings?: Binding[];
+
/**
* @description
* A collection of providers to inject dependencies of the component.
diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts
index 46677271..a8bc1ea3 100644
--- a/projects/testing-library/src/lib/testing-library.ts
+++ b/projects/testing-library/src/lib/testing-library.ts
@@ -11,6 +11,7 @@ import {
SimpleChanges,
Type,
isStandalone,
+ Binding,
} from '@angular/core';
import { ComponentFixture, DeferBlockBehavior, DeferBlockState, TestBed, tick } from '@angular/core/testing';
import { NavigationExtras, Router } from '@angular/router';
@@ -69,6 +70,7 @@ export async function render(
componentOutputs = {},
inputs: newInputs = {},
on = {},
+ bindings = [],
componentProviders = [],
childComponentOverrides = [],
componentImports,
@@ -192,11 +194,37 @@ export async function render(
outputs: Partial,
subscribeTo: OutputRefKeysWithCallback,
): Promise> => {
- const createdFixture: ComponentFixture = await createComponent(componentContainer);
+ const createdFixture: ComponentFixture = await createComponent(componentContainer, bindings);
+
+ // Always apply componentProperties (non-input properties)
setComponentProperties(createdFixture, properties);
- setComponentInputs(createdFixture, inputs);
- setComponentOutputs(createdFixture, outputs);
- subscribedOutputs = subscribeToComponentOutputs(createdFixture, subscribeTo);
+
+ // Angular doesn't allow mixing setInput with bindings
+ // So we use bindings OR traditional approach, but not both for inputs
+ if (bindings && bindings.length > 0) {
+ // When bindings are used, warn if traditional inputs/outputs are also specified
+ if (Object.keys(inputs).length > 0) {
+ console.warn(
+ '[@testing-library/angular]: You specified both bindings and traditional inputs. ' +
+ 'Only bindings will be used for inputs. Use bindings for all inputs to avoid this warning.',
+ );
+ }
+ if (Object.keys(subscribeTo).length > 0) {
+ console.warn(
+ '[@testing-library/angular]: You specified both bindings and traditional output listeners. ' +
+ 'Consider using outputBinding() for all outputs for consistency.',
+ );
+ }
+
+ // Only apply traditional outputs, as bindings handle inputs
+ setComponentOutputs(createdFixture, outputs);
+ subscribedOutputs = subscribeToComponentOutputs(createdFixture, subscribeTo);
+ } else {
+ // Use traditional approach when no bindings
+ setComponentInputs(createdFixture, inputs);
+ setComponentOutputs(createdFixture, outputs);
+ subscribedOutputs = subscribeToComponentOutputs(createdFixture, subscribeTo);
+ }
if (removeAngularAttributes) {
createdFixture.nativeElement.removeAttribute('ng-version');
@@ -335,9 +363,18 @@ export async function render(
};
}
-async function createComponent(component: Type): Promise> {
+async function createComponent(
+ component: Type,
+ bindings?: Binding[],
+): Promise> {
/* Make sure angular application is initialized before creating component */
await TestBed.inject(ApplicationInitStatus).donePromise;
+
+ // Use the new bindings API if available and bindings are provided
+ if (bindings && bindings.length > 0) {
+ return TestBed.createComponent(component, { bindings });
+ }
+
return TestBed.createComponent(component);
}
diff --git a/projects/testing-library/tests/bindings.spec.ts b/projects/testing-library/tests/bindings.spec.ts
new file mode 100644
index 00000000..50718f96
--- /dev/null
+++ b/projects/testing-library/tests/bindings.spec.ts
@@ -0,0 +1,141 @@
+import { Component, input, output, inputBinding, outputBinding, twoWayBinding, signal, model } from '@angular/core';
+import { render, screen, aliasedInput } from '../src/public_api';
+
+describe('Bindings API Support', () => {
+ @Component({
+ selector: 'atl-bindings-test',
+ template: `
+ {{ value() }}
+ {{ greeting() }}
+
+ `,
+ standalone: true,
+ })
+ class BindingsTestComponent {
+ value = input('default');
+ greeting = input('hello', { alias: 'greet' });
+ clicked = output();
+ }
+
+ @Component({
+ selector: 'atl-two-way-test',
+ template: `
+ {{ name() }}
+
+
+ `,
+ standalone: true,
+ })
+ class TwoWayBindingTestComponent {
+ name = model('default');
+
+ updateName() {
+ this.name.set('updated from component');
+ }
+ }
+
+ test('supports inputBinding for regular inputs', async () => {
+ await render(BindingsTestComponent, {
+ bindings: [inputBinding('value', () => 'test-value'), inputBinding('greet', () => 'hi there')],
+ });
+
+ expect(screen.getByTestId('value')).toHaveTextContent('test-value');
+ expect(screen.getByTestId('greeting')).toHaveTextContent('hi there');
+ });
+
+ test('supports outputBinding for outputs', async () => {
+ const clickHandler = jest.fn();
+
+ await render(BindingsTestComponent, {
+ bindings: [inputBinding('value', () => 'bound-value'), outputBinding('clicked', clickHandler)],
+ });
+
+ const button = screen.getByTestId('emit-button');
+ button.click();
+
+ expect(clickHandler).toHaveBeenCalledWith('clicked: bound-value');
+ });
+
+ test('supports inputBinding with writable signal for re-rendering scenario', async () => {
+ const valueSignal = signal('initial-value');
+
+ await render(BindingsTestComponent, {
+ bindings: [inputBinding('value', valueSignal), inputBinding('greet', () => 'hi there')],
+ });
+
+ expect(screen.getByTestId('value')).toHaveTextContent('initial-value');
+ expect(screen.getByTestId('greeting')).toHaveTextContent('hi there');
+
+ // Update the signal and verify it reflects in the component
+ valueSignal.set('updated-value');
+
+ // The binding should automatically update the component
+ expect(await screen.findByText('updated-value')).toBeInTheDocument();
+ });
+
+ test('supports twoWayBinding for model signals', async () => {
+ const nameSignal = signal('initial name');
+
+ await render(TwoWayBindingTestComponent, {
+ bindings: [twoWayBinding('name', nameSignal)],
+ });
+
+ // Verify initial value
+ expect(screen.getByTestId('name-display')).toHaveTextContent('initial name');
+ expect(screen.getByTestId('name-input')).toHaveValue('initial name');
+
+ // Update from outside (signal change)
+ nameSignal.set('updated from signal');
+ expect(await screen.findByDisplayValue('updated from signal')).toBeInTheDocument();
+ expect(screen.getByTestId('name-display')).toHaveTextContent('updated from signal');
+
+ // Update from component - let's trigger change detection after the click
+ const updateButton = screen.getByTestId('update-button');
+ updateButton.click();
+
+ // Give Angular a chance to process the update and check both the signal and display
+ // The twoWayBinding should update the external signal
+ expect(await screen.findByText('updated from component')).toBeInTheDocument();
+ expect(nameSignal()).toBe('updated from component');
+ });
+
+ test('warns when mixing bindings with traditional inputs but still works', async () => {
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
+ const clickHandler = jest.fn();
+ const bindingClickHandler = jest.fn();
+
+ await render(BindingsTestComponent, {
+ bindings: [inputBinding('value', () => 'binding-value'), outputBinding('clicked', bindingClickHandler)],
+ inputs: {
+ ...aliasedInput('greet', 'traditional-greeting'), // This will be ignored due to bindings
+ },
+ on: {
+ clicked: clickHandler, // This should still work alongside bindings
+ },
+ });
+
+ // Only binding should work for inputs
+ expect(screen.getByTestId('value')).toHaveTextContent('binding-value');
+ expect(screen.getByTestId('greeting')).toHaveTextContent('hello'); // Default value, not traditional
+
+ const button = screen.getByTestId('emit-button');
+ button.click();
+
+ // Both binding and traditional handlers are called for outputs
+ expect(bindingClickHandler).toHaveBeenCalledWith('clicked: binding-value');
+ expect(clickHandler).toHaveBeenCalledWith('clicked: binding-value');
+
+ // Shows warning about mixed usage for inputs
+ expect(consoleSpy).toHaveBeenCalledWith(
+ '[@testing-library/angular]: You specified both bindings and traditional inputs. ' +
+ 'Only bindings will be used for inputs. Use bindings for all inputs to avoid this warning.',
+ );
+
+ expect(consoleSpy).toHaveBeenCalledWith(
+ '[@testing-library/angular]: You specified both bindings and traditional output listeners. ' +
+ 'Consider using outputBinding() for all outputs for consistency.',
+ );
+
+ consoleSpy.mockRestore();
+ });
+});
diff --git a/projects/testing-library/tests/config.spec.ts b/projects/testing-library/tests/config.spec.ts
index 041d991a..7783961a 100644
--- a/projects/testing-library/tests/config.spec.ts
+++ b/projects/testing-library/tests/config.spec.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { render, configure, Config } from '../src/public_api';
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
@@ -16,11 +16,10 @@ import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
standalone: false,
})
class FormsComponent {
+ private formBuilder = inject(FormBuilder);
form = this.formBuilder.group({
name: [''],
});
-
- constructor(private formBuilder: FormBuilder) {}
}
let originalConfig: Config;
diff --git a/projects/testing-library/tests/integration.spec.ts b/projects/testing-library/tests/integration.spec.ts
index 02ca2902..70d0169c 100644
--- a/projects/testing-library/tests/integration.spec.ts
+++ b/projects/testing-library/tests/integration.spec.ts
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, Injectable, Input, Output } from '@angular/core';
+import { Component, EventEmitter, inject, Injectable, Input, Output } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { of, BehaviorSubject } from 'rxjs';
import { debounceTime, switchMap, map, startWith } from 'rxjs/operators';
@@ -54,6 +54,8 @@ class TableComponent {
imports: [TableComponent, AsyncPipe],
})
class EntitiesComponent {
+ private entitiesService = inject(EntitiesService);
+ private modalService = inject(ModalService);
query = new BehaviorSubject('');
readonly entities = this.query.pipe(
debounceTime(DEBOUNCE_TIME),
@@ -63,8 +65,6 @@ class EntitiesComponent {
startWith(entities),
);
- constructor(private entitiesService: EntitiesService, private modalService: ModalService) {}
-
newEntityClicked() {
this.modalService.open('new entity');
}
diff --git a/projects/testing-library/tests/issues/issue-280.spec.ts b/projects/testing-library/tests/issues/issue-280.spec.ts
index 711cbec3..ea230e78 100644
--- a/projects/testing-library/tests/issues/issue-280.spec.ts
+++ b/projects/testing-library/tests/issues/issue-280.spec.ts
@@ -1,5 +1,5 @@
import { Location } from '@angular/common';
-import { Component, NgModule } from '@angular/core';
+import { Component, inject, NgModule } from '@angular/core';
import { RouterLink, RouterModule, RouterOutlet, Routes } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import userEvent from '@testing-library/user-event';
@@ -24,7 +24,7 @@ class FirstComponent {}
`,
})
class SecondComponent {
- constructor(private location: Location) {}
+ private location = inject(Location);
goBack() {
this.location.back();
}
diff --git a/projects/testing-library/tests/issues/issue-318.spec.ts b/projects/testing-library/tests/issues/issue-318.spec.ts
index 3f1430e8..1cfe5b85 100644
--- a/projects/testing-library/tests/issues/issue-318.spec.ts
+++ b/projects/testing-library/tests/issues/issue-318.spec.ts
@@ -1,21 +1,20 @@
-import {Component, OnDestroy, OnInit} from '@angular/core';
-import {Router} from '@angular/router';
-import {RouterTestingModule} from '@angular/router/testing';
-import {Subject, takeUntil} from 'rxjs';
-import {render} from "@testing-library/angular";
+import { Component, inject, OnDestroy, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { Subject, takeUntil } from 'rxjs';
+import { render } from '@testing-library/angular';
@Component({
selector: 'atl-app-fixture',
template: '',
})
class FixtureComponent implements OnInit, OnDestroy {
+ private readonly router = inject(Router);
unsubscribe$ = new Subject();
- constructor(private router: Router) {}
-
ngOnInit(): void {
this.router.events.pipe(takeUntil(this.unsubscribe$)).subscribe((evt) => {
- this.eventReceived(evt)
+ this.eventReceived(evt);
});
}
@@ -29,15 +28,13 @@ class FixtureComponent implements OnInit, OnDestroy {
}
}
-
test('it does not invoke router events on init', async () => {
const eventReceived = jest.fn();
await render(FixtureComponent, {
imports: [RouterTestingModule],
componentProperties: {
- eventReceived
- }
+ eventReceived,
+ },
});
expect(eventReceived).not.toHaveBeenCalled();
});
-
diff --git a/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts b/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts
index c2a02a8c..c34e1304 100644
--- a/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts
+++ b/projects/testing-library/tests/issues/issue-397-directive-overrides-component-input.spec.ts
@@ -1,4 +1,4 @@
-import { Component, Directive, Input, OnInit } from '@angular/core';
+import { Component, Directive, inject, Input, OnInit } from '@angular/core';
import { render, screen } from '../../src/public_api';
test('the value set in the directive constructor is overriden by the input binding', async () => {
@@ -48,7 +48,8 @@ class FixtureComponent {
standalone: true,
})
class InputOverrideViaConstructorDirective {
- constructor(private fixture: FixtureComponent) {
+ private readonly fixture = inject(FixtureComponent);
+ constructor() {
this.fixture.input = 'set by directive constructor';
}
}
@@ -59,7 +60,7 @@ class InputOverrideViaConstructorDirective {
standalone: true,
})
class InputOverrideViaOnInitDirective implements OnInit {
- constructor(private fixture: FixtureComponent) {}
+ private readonly fixture = inject(FixtureComponent);
ngOnInit(): void {
this.fixture.input = 'set by directive ngOnInit';
diff --git a/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts b/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts
index c4fa7a37..6dd5bc0c 100644
--- a/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts
+++ b/projects/testing-library/tests/issues/issue-422-view-already-destroyed.spec.ts
@@ -1,4 +1,4 @@
-import { Component, ElementRef } from '@angular/core';
+import { Component, ElementRef, inject } from '@angular/core';
import { NgIf } from '@angular/common';
import { render } from '../../src/public_api';
@@ -9,8 +9,8 @@ test('declaration specific dependencies should be available for components', asy
template: `Test
`,
})
class TestComponent {
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- constructor(_elementRef: ElementRef) {}
+ // @ts-expect-error - testing purpose
+ private _el = inject(ElementRef);
}
await expect(async () => await render(TestComponent)).not.toThrow();
diff --git a/projects/testing-library/tests/issues/issue-435.spec.ts b/projects/testing-library/tests/issues/issue-435.spec.ts
index e1e420f9..2982319b 100644
--- a/projects/testing-library/tests/issues/issue-435.spec.ts
+++ b/projects/testing-library/tests/issues/issue-435.spec.ts
@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { BehaviorSubject } from 'rxjs';
-import { Component, Inject, Injectable } from '@angular/core';
+import { Component, inject, Injectable } from '@angular/core';
import { screen, render } from '../../src/public_api';
// Service
@@ -23,7 +23,7 @@ class DemoService {
`,
})
class DemoComponent {
- constructor(@Inject(DemoService) public demoService: DemoService) {}
+ protected readonly demoService = inject(DemoService);
}
test('issue #435', async () => {
diff --git a/projects/testing-library/tests/issues/issue-493.spec.ts b/projects/testing-library/tests/issues/issue-493.spec.ts
index 5d0e1237..00a39b37 100644
--- a/projects/testing-library/tests/issues/issue-493.spec.ts
+++ b/projects/testing-library/tests/issues/issue-493.spec.ts
@@ -1,6 +1,6 @@
import { HttpClient, provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
-import { Component, input } from '@angular/core';
+import { Component, inject, input } from '@angular/core';
import { render, screen } from '../../src/public_api';
test('succeeds', async () => {
@@ -21,7 +21,7 @@ test('succeeds', async () => {
template: '{{ value() }}
',
})
class DummyComponent {
+ // @ts-expect-error - testing purpose
+ private _http = inject(HttpClient);
value = input.required();
- // @ts-expect-error http is unused but needed for the test
- constructor(private http: HttpClient) {}
}
diff --git a/projects/testing-library/tests/providers/component-provider.spec.ts b/projects/testing-library/tests/providers/component-provider.spec.ts
index 9290d5bd..b774064e 100644
--- a/projects/testing-library/tests/providers/component-provider.spec.ts
+++ b/projects/testing-library/tests/providers/component-provider.spec.ts
@@ -1,4 +1,4 @@
-import { Injectable, Provider } from '@angular/core';
+import { inject, Injectable, Provider } from '@angular/core';
import { Component } from '@angular/core';
import { render, screen } from '../../src/public_api';
@@ -73,5 +73,5 @@ class Service {
providers: [Service],
})
class FixtureComponent {
- constructor(public service: Service) {}
+ protected readonly service = inject(Service);
}
diff --git a/projects/testing-library/tests/providers/module-provider.spec.ts b/projects/testing-library/tests/providers/module-provider.spec.ts
index bd39b81b..80710291 100644
--- a/projects/testing-library/tests/providers/module-provider.spec.ts
+++ b/projects/testing-library/tests/providers/module-provider.spec.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
import { Component } from '@angular/core';
import { render, screen } from '../../src/public_api';
@@ -64,5 +64,5 @@ class Service {
template: '{{service.foo()}}',
})
class FixtureComponent {
- constructor(public service: Service) {}
+ protected readonly service = inject(Service);
}
diff --git a/projects/testing-library/tests/render-template.spec.ts b/projects/testing-library/tests/render-template.spec.ts
index e185f702..cddc28a1 100644
--- a/projects/testing-library/tests/render-template.spec.ts
+++ b/projects/testing-library/tests/render-template.spec.ts
@@ -1,4 +1,4 @@
-import { Directive, HostListener, ElementRef, Input, Output, EventEmitter, Component } from '@angular/core';
+import { Directive, HostListener, ElementRef, Input, Output, EventEmitter, Component, inject } from '@angular/core';
import { render, fireEvent, screen } from '../src/public_api';
@@ -7,11 +7,12 @@ import { render, fireEvent, screen } from '../src/public_api';
selector: '[onOff]',
})
class OnOffDirective {
+ private el = inject(ElementRef);
@Input() on = 'on';
@Input() off = 'off';
@Output() clicked = new EventEmitter();
- constructor(private el: ElementRef) {
+ constructor() {
this.el.nativeElement.textContent = 'init';
}
@@ -26,12 +27,11 @@ class OnOffDirective {
selector: '[update]',
})
class UpdateInputDirective {
+ private readonly el = inject(ElementRef);
@Input()
set update(value: any) {
this.el.nativeElement.textContent = value;
}
-
- constructor(private el: ElementRef) {}
}
@Component({
diff --git a/projects/testing-library/tests/render.spec.ts b/projects/testing-library/tests/render.spec.ts
index a93da906..243a5e81 100644
--- a/projects/testing-library/tests/render.spec.ts
+++ b/projects/testing-library/tests/render.spec.ts
@@ -116,7 +116,7 @@ describe('childComponentOverrides', () => {
providers: [MySimpleService],
})
class NestedChildFixtureComponent {
- public constructor(public simpleService: MySimpleService) {}
+ protected simpleService = inject(MySimpleService);
}
@Component({
@@ -490,7 +490,7 @@ describe('initialRoute', () => {
imports: [NgIf, AsyncPipe],
})
class QueryParamFixtureComponent {
- constructor(public route: ActivatedRoute) {}
+ private readonly route = inject(ActivatedRoute);
paramPresent$ = this.route.queryParams.pipe(map((queryParams) => (queryParams?.param ? 'present' : 'missing')));
}