diff --git a/npm/ng-packs/apps/dev-app/project.json b/npm/ng-packs/apps/dev-app/project.json
index d8b7d778c7..991b7111a7 100644
--- a/npm/ng-packs/apps/dev-app/project.json
+++ b/npm/ng-packs/apps/dev-app/project.json
@@ -166,7 +166,7 @@
"defaultConfiguration": "development"
},
"extract-i18n": {
- "executor": "@angular-devkit/build-angular:extract-i18n",
+ "executor": "@angular/build:extract-i18n",
"options": {
"buildTarget": "dev-app:build"
}
diff --git a/npm/ng-packs/apps/dev-app/src/app/app.component.ts b/npm/ng-packs/apps/dev-app/src/app/app.component.ts
index 57d3ff6695..595f5caed3 100644
--- a/npm/ng-packs/apps/dev-app/src/app/app.component.ts
+++ b/npm/ng-packs/apps/dev-app/src/app/app.component.ts
@@ -1,12 +1,13 @@
-import { Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LoaderBarComponent } from '@abp/ng.theme.shared';
import { DynamicLayoutComponent } from '@abp/ng.core';
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-root',
template: `
-
-
+
+
`,
imports: [LoaderBarComponent, DynamicLayoutComponent],
})
diff --git a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html
index c675dca707..d9f91beae5 100644
--- a/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html
+++ b/npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.html
@@ -20,9 +20,9 @@
All 16 field types + Nested Forms (Group & Array) with full accessibility support
- @if (formFields.length) {
+ @if (formFields().length) {
{
- this.formFields = config;
- });
- }
+ submit(formData: any) {
+ console.log('✅ Form Submitted Successfully!', formData);
+ console.table(formData);
- submit(formData: any) {
- console.log('✅ Form Submitted Successfully!', formData);
- console.table(formData);
-
- // Show success message
- alert('✅ Form submitted successfully! Check the console for details.');
-
- // Reset form after submission
- this.dynamicFormComponent().resetForm();
- }
+ alert('✅ Form submitted successfully! Check the console for details.');
- cancel() {
- console.log('❌ Form Cancelled');
- alert('Form cancelled');
- this.dynamicFormComponent().resetForm();
- }
+ this.dynamicFormComponent()?.resetForm();
+ }
+
+ cancel() {
+ console.log('❌ Form Cancelled');
+ alert('Form cancelled');
+ this.dynamicFormComponent()?.resetForm();
+ }
}
diff --git a/npm/ng-packs/apps/dev-app/src/app/home/home.component.html b/npm/ng-packs/apps/dev-app/src/app/home/home.component.html
index d8b08c06ae..cea534b296 100644
--- a/npm/ng-packs/apps/dev-app/src/app/home/home.component.html
+++ b/npm/ng-packs/apps/dev-app/src/app/home/home.component.html
@@ -16,7 +16,7 @@
{{ '::LongWelcomeMessage' | abpLocalization }}
@if (!hasLoggedIn) {
-
{{ 'AbpAccount::Login' | abpLocalization }}
diff --git a/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts b/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts
index aa25b046d7..00b79e090d 100644
--- a/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts
+++ b/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts
@@ -1,10 +1,11 @@
import { AuthService, LocalizationPipe } from '@abp/ng.core';
-import { Component, inject } from '@angular/core';
+import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { ButtonComponent, CardBodyComponent, CardComponent } from '@abp/ng.theme.shared';
import { RouterLink } from '@angular/router';
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-home',
templateUrl: './home.component.html',
imports: [
@@ -18,14 +19,14 @@ import { RouterLink } from '@angular/router';
})
export class HomeComponent {
protected readonly authService = inject(AuthService);
- loading = false;
+ readonly loading = signal(false);
get hasLoggedIn(): boolean {
return this.authService.isAuthenticated;
}
login() {
- this.loading = true;
+ this.loading.set(true);
this.authService.navigateToLogin();
}
}
diff --git a/npm/ng-packs/apps/dev-app/src/app/localization-test/localization-test.component.ts b/npm/ng-packs/apps/dev-app/src/app/localization-test/localization-test.component.ts
index 187f6d1e37..9ce6a4a80c 100644
--- a/npm/ng-packs/apps/dev-app/src/app/localization-test/localization-test.component.ts
+++ b/npm/ng-packs/apps/dev-app/src/app/localization-test/localization-test.component.ts
@@ -1,13 +1,13 @@
-import { Component, inject, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
import { LocalizationPipe, UILocalizationService, SessionStateService } from '@abp/ng.core';
import { CommonModule } from '@angular/common';
import { CardComponent, CardBodyComponent } from '@abp/ng.theme.shared';
-import { AsyncPipe } from '@angular/common';
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-localization-test',
- standalone: true,
- imports: [CommonModule, LocalizationPipe, CardComponent, CardBodyComponent, AsyncPipe],
+ imports: [CommonModule, LocalizationPipe, CardComponent, CardBodyComponent],
template: `
Hybrid Localization Test
@@ -22,7 +22,7 @@ import { AsyncPipe } from '@angular/common';
- UI Localization (from /assets/localization/{{ currentLanguage$ | async }}.json)
+ UI Localization (from /assets/localization/{{ currentLanguage() }}.json)
MyProjectName::CustomKey: {{ 'MyProjectName::CustomKey' | abpLocalization }}
MyProjectName::TestMessage: {{ 'MyProjectName::TestMessage' | abpLocalization }}
@@ -39,20 +39,19 @@ import { AsyncPipe } from '@angular/common';
Loaded UI Localizations
- {{ loadedLocalizations | json }}
+ {{ loadedLocalizations() | json }}
`,
})
-export class LocalizationTestComponent implements OnInit {
+export class LocalizationTestComponent {
private uiLocalizationService = inject(UILocalizationService);
private sessionState = inject(SessionStateService);
- loadedLocalizations: any = {};
- currentLanguage$ = this.sessionState.getLanguage$();
+ readonly loadedLocalizations = signal(this.uiLocalizationService.getLoadedLocalizations());
- ngOnInit() {
- this.loadedLocalizations = this.uiLocalizationService.getLoadedLocalizations();
- }
+ readonly currentLanguage = toSignal(this.sessionState.getLanguage$(), {
+ initialValue: this.sessionState.getLanguage(),
+ });
}
diff --git a/npm/ng-packs/apps/dev-app/tsconfig.json b/npm/ng-packs/apps/dev-app/tsconfig.json
index f02580de5c..df2232aed6 100644
--- a/npm/ng-packs/apps/dev-app/tsconfig.json
+++ b/npm/ng-packs/apps/dev-app/tsconfig.json
@@ -28,6 +28,6 @@
"angularCompilerOptions": {
"strictInjectionParameters": false,
"strictInputAccessModifiers": false,
- "strictTemplates": false
+ "strictTemplates": true
}
}
diff --git a/npm/ng-packs/guides/DEVELOPMENT_GUIDE.md b/npm/ng-packs/guides/DEVELOPMENT_GUIDE.md
index a44fd2a49d..9bc8e964fb 100644
--- a/npm/ng-packs/guides/DEVELOPMENT_GUIDE.md
+++ b/npm/ng-packs/guides/DEVELOPMENT_GUIDE.md
@@ -222,12 +222,52 @@ Example HTML template
### Component Best Practices
1. **Single Responsibility**: Each component should have one clear purpose
-2. **Dependency Injection**: Use constructor injection for services
-3. **Lifecycle Management**: Implement `OnInit` and `OnDestroy` when needed
-4. **State Management**: Use reactive forms and observables
-5. **Error Handling**: Implement proper error boundaries
-6. **Accessibility**: Follow ARIA guidelines
-7. **Performance**: Use `OnPush` change detection when possible
+2. **Dependency Injection**: Use `inject()` for services
+3. **Change detection**: Set `changeDetection: ChangeDetectionStrategy.OnPush` explicitly on every component
+4. **Async state**: Use `signal()` / `computed()` / `toSignal()` — avoid mutating plain properties in `.subscribe()` without a CD trigger
+5. **List pages**: Use `toSignal(this.list.hookToQuery(...))` and bind `data().items` in templates
+6. **Component IO**: Prefer signal `input()` / `output()` / `model()` over `@Input` / `@Output`
+7. **Modal bindings**: `[visible]="isModalVisible()" (visibleChange)="isModalVisible.set($event)"` with `[busy]="modalBusy()"`
+8. **`markForCheck()`**: Discouraged — use only in directives, CVAs, or dynamic component hosts where signals cannot apply
+9. **Error Handling**: Implement proper error boundaries
+10. **Accessibility**: Follow ARIA guidelines
+
+See [upgrade-notes.md](../upgrade-notes.md) for Angular 22 customer migration guidance.
+
+### Angular 22 component pattern (recommended)
+
+```typescript
+import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { ListService, PagedResultDto } from '@abp/ng.core';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'abp-my-list',
+ providers: [ListService],
+})
+export class MyListComponent {
+ protected readonly list = inject(ListService);
+ protected readonly service = inject(MyService);
+
+ readonly data = toSignal(this.list.hookToQuery(q => this.service.getList(q)), {
+ initialValue: { items: [], totalCount: 0 } as PagedResultDto,
+ });
+
+ readonly isModalVisible = signal(false);
+ readonly modalBusy = signal(false);
+}
+```
+
+```html
+
+
+
+```
## Service Layer