diff --git a/apps/401-recipe-filter-solution/src/app/recipe/recipe-filter.ng.ts b/apps/401-recipe-filter-solution/src/app/recipe/recipe-filter.ng.ts
index 3af28c4..4595496 100644
--- a/apps/401-recipe-filter-solution/src/app/recipe/recipe-filter.ng.ts
+++ b/apps/401-recipe-filter-solution/src/app/recipe/recipe-filter.ng.ts
@@ -1,53 +1,72 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
-import { outputFromObservable } from '@angular/core/rxjs-interop';
-import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
-import { map } from 'rxjs/operators';
-import { createRecipeFilterCriteria } from './recipe-filter-criteria';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ model,
+ output,
+ signal,
+} from '@angular/core';
+import { form, FormField, FormRoot } from '@angular/forms/signals';
+import { RecipeFilterCriteria } from './recipe-filter-criteria';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wm-recipe-filter',
- imports: [ReactiveFormsModule],
+ imports: [FormField, FormRoot],
template: `
-
`,
- styles: [
- `
- :host {
- text-align: center;
- }
- `,
- ],
+ styles: `
+ :host {
+ text-align: center;
+ }
+ `,
})
export class RecipeFilter {
- protected filterFormGroup = new FormGroup({
- keywords: new FormControl(),
- maxIngredientCount: new FormControl(),
- maxStepCount: new FormControl(),
- });
+ filterChange = output();
- filterChange = outputFromObservable(
- this.filterFormGroup.valueChanges.pipe(
- map((value) => createRecipeFilterCriteria(value)),
- ),
+ filterForm = form(
+ signal<{
+ keywords: string;
+ maxIngredientCount: number | null;
+ maxStepCount: number | null;
+ }>({
+ keywords: '',
+ maxIngredientCount: null,
+ maxStepCount: null,
+ }),
);
+
+ constructor() {
+ effect(() => {
+ const { keywords, maxIngredientCount, maxStepCount } =
+ this.filterForm().value();
+ this.filterChange.emit({
+ keywords: keywords.length > 0 ? keywords : undefined,
+ maxIngredientCount:
+ maxIngredientCount != null ? maxIngredientCount : undefined,
+ maxStepCount: maxStepCount != null ? maxStepCount : undefined,
+ });
+ });
+ }
}
diff --git a/apps/401-recipe-filter-starter/src/app/recipe/recipe-filter.ng.ts b/apps/401-recipe-filter-starter/src/app/recipe/recipe-filter.ng.ts
index 25af471..9a79368 100644
--- a/apps/401-recipe-filter-starter/src/app/recipe/recipe-filter.ng.ts
+++ b/apps/401-recipe-filter-starter/src/app/recipe/recipe-filter.ng.ts
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, output } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
-import { RecipeFilterCriteria } from './recipe-filter';
+import { RecipeFilterCriteria } from './recipe-filter-criteria';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
diff --git a/apps/402-recipe-search-filter-interaction-solution/src/app/recipe/recipe-filter.ng.ts b/apps/402-recipe-search-filter-interaction-solution/src/app/recipe/recipe-filter.ng.ts
index 3af28c4..4595496 100644
--- a/apps/402-recipe-search-filter-interaction-solution/src/app/recipe/recipe-filter.ng.ts
+++ b/apps/402-recipe-search-filter-interaction-solution/src/app/recipe/recipe-filter.ng.ts
@@ -1,53 +1,72 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
-import { outputFromObservable } from '@angular/core/rxjs-interop';
-import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
-import { map } from 'rxjs/operators';
-import { createRecipeFilterCriteria } from './recipe-filter-criteria';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ model,
+ output,
+ signal,
+} from '@angular/core';
+import { form, FormField, FormRoot } from '@angular/forms/signals';
+import { RecipeFilterCriteria } from './recipe-filter-criteria';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wm-recipe-filter',
- imports: [ReactiveFormsModule],
+ imports: [FormField, FormRoot],
template: `
-
`,
- styles: [
- `
- :host {
- text-align: center;
- }
- `,
- ],
+ styles: `
+ :host {
+ text-align: center;
+ }
+ `,
})
export class RecipeFilter {
- protected filterFormGroup = new FormGroup({
- keywords: new FormControl(),
- maxIngredientCount: new FormControl(),
- maxStepCount: new FormControl(),
- });
+ filterChange = output();
- filterChange = outputFromObservable(
- this.filterFormGroup.valueChanges.pipe(
- map((value) => createRecipeFilterCriteria(value)),
- ),
+ filterForm = form(
+ signal<{
+ keywords: string;
+ maxIngredientCount: number | null;
+ maxStepCount: number | null;
+ }>({
+ keywords: '',
+ maxIngredientCount: null,
+ maxStepCount: null,
+ }),
);
+
+ constructor() {
+ effect(() => {
+ const { keywords, maxIngredientCount, maxStepCount } =
+ this.filterForm().value();
+ this.filterChange.emit({
+ keywords: keywords.length > 0 ? keywords : undefined,
+ maxIngredientCount:
+ maxIngredientCount != null ? maxIngredientCount : undefined,
+ maxStepCount: maxStepCount != null ? maxStepCount : undefined,
+ });
+ });
+ }
}
diff --git a/apps/402-recipe-search-filter-interaction-starter/src/app/recipe/recipe-filter.ng.ts b/apps/402-recipe-search-filter-interaction-starter/src/app/recipe/recipe-filter.ng.ts
index 3af28c4..4595496 100644
--- a/apps/402-recipe-search-filter-interaction-starter/src/app/recipe/recipe-filter.ng.ts
+++ b/apps/402-recipe-search-filter-interaction-starter/src/app/recipe/recipe-filter.ng.ts
@@ -1,53 +1,72 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
-import { outputFromObservable } from '@angular/core/rxjs-interop';
-import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
-import { map } from 'rxjs/operators';
-import { createRecipeFilterCriteria } from './recipe-filter-criteria';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ model,
+ output,
+ signal,
+} from '@angular/core';
+import { form, FormField, FormRoot } from '@angular/forms/signals';
+import { RecipeFilterCriteria } from './recipe-filter-criteria';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wm-recipe-filter',
- imports: [ReactiveFormsModule],
+ imports: [FormField, FormRoot],
template: `
-
`,
- styles: [
- `
- :host {
- text-align: center;
- }
- `,
- ],
+ styles: `
+ :host {
+ text-align: center;
+ }
+ `,
})
export class RecipeFilter {
- protected filterFormGroup = new FormGroup({
- keywords: new FormControl(),
- maxIngredientCount: new FormControl(),
- maxStepCount: new FormControl(),
- });
+ filterChange = output();
- filterChange = outputFromObservable(
- this.filterFormGroup.valueChanges.pipe(
- map((value) => createRecipeFilterCriteria(value)),
- ),
+ filterForm = form(
+ signal<{
+ keywords: string;
+ maxIngredientCount: number | null;
+ maxStepCount: number | null;
+ }>({
+ keywords: '',
+ maxIngredientCount: null,
+ maxStepCount: null,
+ }),
);
+
+ constructor() {
+ effect(() => {
+ const { keywords, maxIngredientCount, maxStepCount } =
+ this.filterForm().value();
+ this.filterChange.emit({
+ keywords: keywords.length > 0 ? keywords : undefined,
+ maxIngredientCount:
+ maxIngredientCount != null ? maxIngredientCount : undefined,
+ maxStepCount: maxStepCount != null ? maxStepCount : undefined,
+ });
+ });
+ }
}
diff --git a/apps/403-recipe-search-add-button-solution/src/app/recipe/recipe-filter.ng.ts b/apps/403-recipe-search-add-button-solution/src/app/recipe/recipe-filter.ng.ts
index 3af28c4..4595496 100644
--- a/apps/403-recipe-search-add-button-solution/src/app/recipe/recipe-filter.ng.ts
+++ b/apps/403-recipe-search-add-button-solution/src/app/recipe/recipe-filter.ng.ts
@@ -1,53 +1,72 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
-import { outputFromObservable } from '@angular/core/rxjs-interop';
-import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
-import { map } from 'rxjs/operators';
-import { createRecipeFilterCriteria } from './recipe-filter-criteria';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ model,
+ output,
+ signal,
+} from '@angular/core';
+import { form, FormField, FormRoot } from '@angular/forms/signals';
+import { RecipeFilterCriteria } from './recipe-filter-criteria';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wm-recipe-filter',
- imports: [ReactiveFormsModule],
+ imports: [FormField, FormRoot],
template: `
-
`,
- styles: [
- `
- :host {
- text-align: center;
- }
- `,
- ],
+ styles: `
+ :host {
+ text-align: center;
+ }
+ `,
})
export class RecipeFilter {
- protected filterFormGroup = new FormGroup({
- keywords: new FormControl(),
- maxIngredientCount: new FormControl(),
- maxStepCount: new FormControl(),
- });
+ filterChange = output();
- filterChange = outputFromObservable(
- this.filterFormGroup.valueChanges.pipe(
- map((value) => createRecipeFilterCriteria(value)),
- ),
+ filterForm = form(
+ signal<{
+ keywords: string;
+ maxIngredientCount: number | null;
+ maxStepCount: number | null;
+ }>({
+ keywords: '',
+ maxIngredientCount: null,
+ maxStepCount: null,
+ }),
);
+
+ constructor() {
+ effect(() => {
+ const { keywords, maxIngredientCount, maxStepCount } =
+ this.filterForm().value();
+ this.filterChange.emit({
+ keywords: keywords.length > 0 ? keywords : undefined,
+ maxIngredientCount:
+ maxIngredientCount != null ? maxIngredientCount : undefined,
+ maxStepCount: maxStepCount != null ? maxStepCount : undefined,
+ });
+ });
+ }
}
diff --git a/apps/403-recipe-search-add-button-starter/src/app/recipe/recipe-filter.ng.ts b/apps/403-recipe-search-add-button-starter/src/app/recipe/recipe-filter.ng.ts
index 3af28c4..4595496 100644
--- a/apps/403-recipe-search-add-button-starter/src/app/recipe/recipe-filter.ng.ts
+++ b/apps/403-recipe-search-add-button-starter/src/app/recipe/recipe-filter.ng.ts
@@ -1,53 +1,72 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
-import { outputFromObservable } from '@angular/core/rxjs-interop';
-import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
-import { map } from 'rxjs/operators';
-import { createRecipeFilterCriteria } from './recipe-filter-criteria';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ model,
+ output,
+ signal,
+} from '@angular/core';
+import { form, FormField, FormRoot } from '@angular/forms/signals';
+import { RecipeFilterCriteria } from './recipe-filter-criteria';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wm-recipe-filter',
- imports: [ReactiveFormsModule],
+ imports: [FormField, FormRoot],
template: `
-
`,
- styles: [
- `
- :host {
- text-align: center;
- }
- `,
- ],
+ styles: `
+ :host {
+ text-align: center;
+ }
+ `,
})
export class RecipeFilter {
- protected filterFormGroup = new FormGroup({
- keywords: new FormControl(),
- maxIngredientCount: new FormControl(),
- maxStepCount: new FormControl(),
- });
+ filterChange = output();
- filterChange = outputFromObservable(
- this.filterFormGroup.valueChanges.pipe(
- map((value) => createRecipeFilterCriteria(value)),
- ),
+ filterForm = form(
+ signal<{
+ keywords: string;
+ maxIngredientCount: number | null;
+ maxStepCount: number | null;
+ }>({
+ keywords: '',
+ maxIngredientCount: null,
+ maxStepCount: null,
+ }),
);
+
+ constructor() {
+ effect(() => {
+ const { keywords, maxIngredientCount, maxStepCount } =
+ this.filterForm().value();
+ this.filterChange.emit({
+ keywords: keywords.length > 0 ? keywords : undefined,
+ maxIngredientCount:
+ maxIngredientCount != null ? maxIngredientCount : undefined,
+ maxStepCount: maxStepCount != null ? maxStepCount : undefined,
+ });
+ });
+ }
}
diff --git a/apps/404-recipe-filter-material-solution/src/app/recipe/recipe-filter.ng.ts b/apps/404-recipe-filter-material-solution/src/app/recipe/recipe-filter.ng.ts
index 3e0c05c..8a224e5 100644
--- a/apps/404-recipe-filter-material-solution/src/app/recipe/recipe-filter.ng.ts
+++ b/apps/404-recipe-filter-material-solution/src/app/recipe/recipe-filter.ng.ts
@@ -1,51 +1,71 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
-import { outputFromObservable } from '@angular/core/rxjs-interop';
-import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
-import { MatFormFieldModule } from '@angular/material/form-field';
-import { MatInputModule } from '@angular/material/input';
-import { map } from 'rxjs/operators';
-import { createRecipeFilterCriteria } from './recipe-filter-criteria';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ signal,
+ output,
+ effect,
+} from '@angular/core';
+import { form, FormField, FormRoot } from '@angular/forms/signals';
+import { RecipeFilterCriteria } from './recipe-filter-criteria';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wm-recipe-filter',
- imports: [MatFormFieldModule, MatInputModule, ReactiveFormsModule],
+ imports: [FormField, FormRoot],
template: `
-
`,
- styles: [
- `
- :host {
- text-align: center;
- }
- `,
- ],
+ styles: `
+ :host {
+ text-align: center;
+ }
+ `,
})
export class RecipeFilter {
- protected filterFormGroup = new FormGroup({
- keywords: new FormControl(),
- maxIngredientCount: new FormControl(),
- maxStepCount: new FormControl(),
- });
+ filterChange = output();
- filterChange = outputFromObservable(
- this.filterFormGroup.valueChanges.pipe(
- map((value) => createRecipeFilterCriteria(value)),
- ),
+ filterForm = form(
+ signal<{
+ keywords: string;
+ maxIngredientCount: number | null;
+ maxStepCount: number | null;
+ }>({
+ keywords: '',
+ maxIngredientCount: null,
+ maxStepCount: null,
+ }),
);
+
+ constructor() {
+ effect(() => {
+ const { keywords, maxIngredientCount, maxStepCount } =
+ this.filterForm().value();
+ this.filterChange.emit({
+ keywords: keywords.length > 0 ? keywords : undefined,
+ maxIngredientCount:
+ maxIngredientCount != null ? maxIngredientCount : undefined,
+ maxStepCount: maxStepCount != null ? maxStepCount : undefined,
+ });
+ });
+ }
}
diff --git a/apps/404-recipe-filter-material-starter/src/app/recipe/recipe-filter.ng.ts b/apps/404-recipe-filter-material-starter/src/app/recipe/recipe-filter.ng.ts
index 3af28c4..4595496 100644
--- a/apps/404-recipe-filter-material-starter/src/app/recipe/recipe-filter.ng.ts
+++ b/apps/404-recipe-filter-material-starter/src/app/recipe/recipe-filter.ng.ts
@@ -1,53 +1,72 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
-import { outputFromObservable } from '@angular/core/rxjs-interop';
-import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
-import { map } from 'rxjs/operators';
-import { createRecipeFilterCriteria } from './recipe-filter-criteria';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ model,
+ output,
+ signal,
+} from '@angular/core';
+import { form, FormField, FormRoot } from '@angular/forms/signals';
+import { RecipeFilterCriteria } from './recipe-filter-criteria';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wm-recipe-filter',
- imports: [ReactiveFormsModule],
+ imports: [FormField, FormRoot],
template: `
-
`,
- styles: [
- `
- :host {
- text-align: center;
- }
- `,
- ],
+ styles: `
+ :host {
+ text-align: center;
+ }
+ `,
})
export class RecipeFilter {
- protected filterFormGroup = new FormGroup({
- keywords: new FormControl(),
- maxIngredientCount: new FormControl(),
- maxStepCount: new FormControl(),
- });
+ filterChange = output();
- filterChange = outputFromObservable(
- this.filterFormGroup.valueChanges.pipe(
- map((value) => createRecipeFilterCriteria(value)),
- ),
+ filterForm = form(
+ signal<{
+ keywords: string;
+ maxIngredientCount: number | null;
+ maxStepCount: number | null;
+ }>({
+ keywords: '',
+ maxIngredientCount: null,
+ maxStepCount: null,
+ }),
);
+
+ constructor() {
+ effect(() => {
+ const { keywords, maxIngredientCount, maxStepCount } =
+ this.filterForm().value();
+ this.filterChange.emit({
+ keywords: keywords.length > 0 ? keywords : undefined,
+ maxIngredientCount:
+ maxIngredientCount != null ? maxIngredientCount : undefined,
+ maxStepCount: maxStepCount != null ? maxStepCount : undefined,
+ });
+ });
+ }
}
diff --git a/tools/downgrade-to-jest.ts b/tools/downgrade-to-jest.ts
index 1930f75..5bdeca7 100644
--- a/tools/downgrade-to-jest.ts
+++ b/tools/downgrade-to-jest.ts
@@ -36,12 +36,17 @@ function donwgradeNxJsonUpdater(nxJson: NxJsonConfiguration) {
return plugin;
}
+ const options = (plugin.options ?? {}) as Record;
+ if (options && typeof options !== 'object') {
+ throw new Error(`Invalid plugin options: ${JSON.stringify(options)}`);
+ }
+
switch (plugin.plugin) {
case '@nx/jest/plugin':
return {
...plugin,
options: {
- ...(plugin.options ?? {}),
+ ...options,
targetName: 'test',
},
};
@@ -49,7 +54,7 @@ function donwgradeNxJsonUpdater(nxJson: NxJsonConfiguration) {
return {
...plugin,
options: {
- ...(plugin.options ?? {}),
+ ...options,
testTargetName: 'vitest',
},
};
diff --git a/tools/tsconfig.tools.json b/tools/tsconfig.tools.json
index 982c47f..a90f956 100644
--- a/tools/tsconfig.tools.json
+++ b/tools/tsconfig.tools.json
@@ -1,13 +1,14 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
- "outDir": "../dist/out-tsc/tools",
- "rootDir": ".",
+ "allowImportingTsExtensions": true,
+ "importHelpers": false,
"module": "esnext",
"moduleResolution": "bundler",
+ "outDir": "../dist/out-tsc/tools",
+ "rootDir": ".",
"target": "esnext",
"types": ["node"],
- "importHelpers": false
},
"include": ["**/*.ts"]
}