Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/angular/add-to-existing.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const appConfig: ApplicationConfig = {
};
```

This reflects the Angular 21 scaffold, which is zoneless by default. If your existing app is on Angular 18 through 20, it still has `provideZoneChangeDetection({ eventCoalescing: true })`; keep that provider and add `provideIonicAngular({})` alongside it. Refer to [Zoneless Change Detection](/docs/angular/zoneless.md) for details.
This reflects the Angular 21 and 22 scaffold, which is zoneless by default. If your existing app is on Angular 18 through 20, it still has `provideZoneChangeDetection({ eventCoalescing: true })`; keep that provider and add `provideIonicAngular({})` alongside it. Refer to [Zoneless Change Detection](/docs/angular/zoneless.md) for details.

## Using Individual Components

Expand Down
6 changes: 5 additions & 1 deletion docs/angular/zoneless.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ sidebar_label: Zoneless
/>
</head>

Angular 21 makes [zoneless change detection](https://angular.dev/guide/zoneless) the default, removing the dependency on Zone.js. This guide covers what you need to know to run an Ionic Angular app without Zone.js.
Angular 21 made [zoneless change detection](https://angular.dev/guide/zoneless) the default, removing the dependency on Zone.js. This guide covers what you need to know to run an Ionic Angular app without Zone.js.

With Zone.js, Angular automatically re-renders after almost any asynchronous task. Without it, Angular only re-renders when you explicitly tell it the view is out of date. Most of your app keeps working unchanged, but a few patterns that relied on Zone.js need a small adjustment.

Expand All @@ -26,6 +26,10 @@ You do not need to change these. Angular schedules change detection for them in
- Ionic page lifecycle hooks (`ionViewWillEnter`, `ionViewDidEnter`, `ionViewWillLeave`, `ionViewDidLeave`) that set state synchronously. Ionic notifies Angular after each hook runs.
- Navigation, route transitions, and tab switching.

:::note Angular 22
Angular 22 also makes `OnPush` the default change detection strategy. Under `OnPush`, synchronous state set as a plain field (including in the lifecycle hooks above) no longer re-renders on its own, even though Ionic notifies Angular. Signals still update the view. For the migration path, refer to the [OnPush Change Detection section of the Ionic 9 upgrade guide](/docs/updating/9-0.md#onpush-change-detection-on-angular-22).
:::

## What needs a notification

When you update component state from an asynchronous callback that Angular did not wrap, nothing schedules a re-render. The state changes, but the view does not update. This applies to any Angular code, not only Ionic, and the common sources in an Ionic app are:
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The Ionic team has compiled a set of recommendations for using the Ionic Framewo

| Framework | Minimum Angular Version | Maximum Angular Version | TypeScript |
| :-------: | :---------------------: | :---------------------: | :--------: |
| v9 | v18 | v21.x | 5.4+[^4] |
| v9 | v18 | v22.x | 5.4+[^4] |
| v8 | v16 | v20.x[^3] | 4.9.3+ |
| v7 | v14 | v17.x[^2] | 4.6+ |
| v6 | v12 | v15.x[^1] | 4.0+ |
Expand All @@ -55,7 +55,7 @@ The Ionic team has compiled a set of recommendations for using the Ionic Framewo
[^1]: Angular 14.x supported starting in Ionic v6.1.9. Angular 15.x supported starting in Ionic v6.3.6.
[^2]: Angular 17.x supported starting in Ionic v7.5.4.
[^3]: Angular 18.x supported starting in Ionic v8.2.0.
[^4]: Ionic v9 supports TypeScript 5.4+ for compatibility with Angular 18. Using Angular 21 requires TypeScript 5.9 or later, per Angular's own requirements.
[^4]: Ionic v9 supports TypeScript 5.4+ for compatibility with Angular 18. Using Angular 21 requires TypeScript 5.9 or later, and Angular 22 requires TypeScript 6.0 or later, per Angular's own requirements.

**Angular 13+ Support On Older Versions of iOS**

Expand Down
39 changes: 34 additions & 5 deletions docs/updating/9-0.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ For a **complete list of breaking changes** from Ionic 8 to Ionic 9, please refe

### Angular

1. Ionic 9 supports Angular 18 through 21. Angular 16 and 17 are no longer supported. Update to a supported version of Angular by following the [Angular Update Guide](https://update.angular.io/).
1. Ionic 9 supports Angular 18 through 22. Angular 16 and 17 are no longer supported. Update to a supported version of Angular by following the [Angular Update Guide](https://update.angular.io/).

2. Update to the latest version of Ionic 9:

Expand All @@ -32,7 +32,7 @@ npm install @ionic/angular@latest @ionic/angular-server@latest @ionic/angular-to

#### Zoneless Change Detection

Ionic 9 supports zoneless change detection. Angular 21 makes zoneless the default, so a new Ionic 9 app on Angular 21 runs without Zone.js out of the box and no change-detection provider is required.
Ionic 9 supports zoneless change detection. Angular 21 made zoneless the default, so a new Ionic 9 app on Angular 21 or later runs without Zone.js out of the box and no change-detection provider is required.

Without Zone.js, Angular does not automatically re-render when you update component state from an asynchronous callback (awaiting an overlay result, `setTimeout`, RxJS subscriptions, `Platform` events). In those cases you must notify Angular with a signal or `ChangeDetectorRef.markForCheck()`. Refer to [Zoneless Change Detection](/docs/angular/zoneless.md) for the patterns Ionic apps use.

Expand All @@ -42,7 +42,7 @@ On Angular 18 through 20, Zone.js remains Angular's default, so those versions a

##### Keeping Zone.js

If you prefer to keep using Zone.js on Angular 21, opt back in with `provideZoneChangeDetection()`.
If you prefer to keep using Zone.js on Angular 21 or later, opt back in with `provideZoneChangeDetection()`.

For standalone applications, add the provider to `bootstrapApplication`:

Expand Down Expand Up @@ -72,15 +72,44 @@ platformBrowserDynamic()
.catch((err) => console.error(err));
```

Either way, also confirm `zone.js` is listed in the `polyfills` array in `angular.json`. Angular 21's default scaffold omits it:
Either way, also confirm `zone.js` is listed in the `polyfills` array in `angular.json`. Angular 21 and later default scaffolds omit it:

```json title="angular.json"
"polyfills": ["zone.js"]
```

#### OnPush Change Detection on Angular 22

Angular 22 changes the default change detection strategy to `OnPush` for components that don't declare one. Combined with the zoneless default above, component state you mutate as a plain field from an Ionic lifecycle hook (`ionViewWillEnter`, and so on) no longer re-renders on its own.

Run `ng update` when upgrading to Angular 22; it migrates your existing components to eager change detection and preserves the previous behavior. To write `OnPush`-ready components instead, set the state through a signal or call `ChangeDetectorRef.markForCheck()` in the hook:

```diff
+ import { signal } from '@angular/core';
+
- entered = 0;
-
- ionViewWillEnter() {
- this.entered++;
- }
+ entered = signal(0);
+
+ ionViewWillEnter() {
+ this.entered.update((count) => count + 1);
+ }
```

:::note
Ionic's own Angular components already declare `OnPush`, so they are unaffected. Angular 18 through 21 keep the eager default and require no change.
:::

#### TypeScript

Ionic 9 supports TypeScript 5.4 or later, matching the minimum for Angular 18. If you upgrade to Angular 21, it requires TypeScript 5.9 or later.
Ionic 9 supports TypeScript 5.4 or later, matching the minimum for Angular 18. Angular 21 requires TypeScript 5.9 or later, and Angular 22 requires TypeScript 6.0 or later.

#### Node.js

Angular 22 raises the minimum Node.js version to `^22.22.3 || ^24.15.0 || ^26.0.0`. Angular 18 through 21 are unaffected.

#### CSS Imports

Expand Down
23 changes: 14 additions & 9 deletions static/code/stackblitz/v9/angular/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions static/code/stackblitz/v9/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"@angular/platform-browser": "^21.0.0",
"@angular/platform-browser-dynamic": "^21.0.0",
"@angular/router": "^21.0.0",
"@ionic/angular": "8.8.9-dev.11780414486.1df2bd72",
"@ionic/core": "8.8.9-dev.11780414486.1df2bd72",
"@ionic/angular": "8.8.9-dev.11781098612.122c6758",
"@ionic/core": "8.8.9-dev.11781098612.122c6758",
"ionicons": "8.0.13",
"rxjs": "^7.8.1",
"tslib": "^2.5.0",
Expand Down
Comment thread
brandyscarney marked this conversation as resolved.
Comment thread
ShaneK marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
</ion-header>
<ion-content class="ion-padding">
<ion-button expand="block" (click)="openModal()">Open</ion-button>
<p>{{ message }}</p>
<p>{{ message() }}</p>
</ion-content>
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
```ts
import { Component } from '@angular/core';
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { IonButton, IonContent, IonHeader, IonTitle, IonToolbar, ModalController } from '@ionic/angular/standalone';

Expand All @@ -12,7 +12,7 @@ import { ModalExampleComponent } from './modal-example.component';
imports: [FormsModule, IonButton, IonContent, IonHeader, IonTitle, IonToolbar],
})
export class ExampleComponent {
message = 'This modal example uses the modalController to present and dismiss modals.';
readonly message = signal('This modal example uses the modalController to present and dismiss modals.');

constructor(private modalCtrl: ModalController) {}

Expand All @@ -25,7 +25,7 @@ export class ExampleComponent {
const { data, role } = await modal.onWillDismiss();

if (role === 'confirm') {
this.message = `Hello, ${data}!`;
this.message.set(`Hello, ${data}!`);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
IonButtons,
IonContent,
IonHeader,
IonInput,
IonItem,
IonTitle,
IonToolbar,
Expand All @@ -16,7 +17,7 @@ import {
@Component({
selector: 'app-modal-example',
templateUrl: 'modal-example.component.html',
imports: [FormsModule, IonButton, IonButtons, IonContent, IonHeader, IonItem, IonTitle, IonToolbar],
imports: [FormsModule, IonButton, IonButtons, IonContent, IonHeader, IonInput, IonItem, IonTitle, IonToolbar],
})
export class ModalExampleComponent {
name!: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
```html
<ion-progress-bar [buffer]="buffer" [value]="progress"></ion-progress-bar>
<ion-progress-bar [buffer]="buffer()" [value]="progress()"></ion-progress-bar>
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
```ts
import { Component } from '@angular/core';
import { Component, signal } from '@angular/core';
import { IonProgressBar } from '@ionic/angular/standalone';

@Component({
Expand All @@ -9,20 +9,20 @@ import { IonProgressBar } from '@ionic/angular/standalone';
imports: [IonProgressBar],
})
export class ExampleComponent {
public buffer = 0.06;
public progress = 0;
readonly buffer = signal(0.06);
readonly progress = signal(0);

constructor() {
setInterval(() => {
this.buffer += 0.06;
this.progress += 0.06;
this.buffer.update((value) => value + 0.06);
this.progress.update((value) => value + 0.06);

// Reset the progress bar when it reaches 100%
// to continuously show the demo
if (this.progress > 1) {
if (this.progress() > 1) {
setTimeout(() => {
this.buffer = 0.06;
this.progress = 0;
this.buffer.set(0.06);
this.progress.set(0);
}, 1000);
}
}, 1000);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
```html
<ion-progress-bar [value]="progress"></ion-progress-bar>
<ion-progress-bar [value]="progress()"></ion-progress-bar>
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
```ts
import { Component } from '@angular/core';
import { Component, signal } from '@angular/core';
import { IonProgressBar } from '@ionic/angular/standalone';

@Component({
Expand All @@ -9,17 +9,17 @@ import { IonProgressBar } from '@ionic/angular/standalone';
imports: [IonProgressBar],
})
export class ExampleComponent {
public progress = 0;
readonly progress = signal(0);

constructor() {
setInterval(() => {
this.progress += 0.01;
this.progress.update((value) => value + 0.01);

// Reset the progress bar when it reaches 100%
// to continuously show the demo
if (this.progress > 1) {
if (this.progress() > 1) {
setTimeout(() => {
this.progress = 0;
this.progress.set(0);
}, 1000);
}
}, 50);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<ion-list-header>Appearance</ion-list-header>
<ion-list [inset]="true">
<ion-item>
<ion-toggle [(ngModel)]="paletteToggle" (ionChange)="toggleChange($event)" justify="space-between"
<ion-toggle [checked]="paletteToggle()" (ionChange)="toggleChange($event)" justify="space-between"
>Dark Mode</ion-toggle
>
</ion-item>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
```ts
import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Component, OnInit, signal } from '@angular/core';
import {
IonBackButton,
IonButton,
Expand All @@ -27,7 +26,6 @@ import { personCircle, personCircleOutline, sunny, sunnyOutline } from 'ionicons
templateUrl: 'example.component.html',
styleUrls: ['example.component.css'],
imports: [
FormsModule,
IonBackButton,
IonButton,
IonButtons,
Expand All @@ -46,7 +44,9 @@ import { personCircle, personCircleOutline, sunny, sunnyOutline } from 'ionicons
],
})
export class ExampleComponent implements OnInit {
paletteToggle = false;
// Backs the toggle's checked state. Using a signal keeps the toggle in sync
// when the value is updated from the matchMedia listener, even without Zone.js.
readonly paletteToggle = signal(false);

constructor() {
/**
Expand All @@ -71,13 +71,13 @@ export class ExampleComponent implements OnInit {

// Check/uncheck the toggle and update the palette based on isDark
initializeDarkPalette(isDark: boolean) {
this.paletteToggle = isDark;
this.paletteToggle.set(isDark);
this.toggleDarkPalette(isDark);
}

// Listen for the toggle check/uncheck to toggle the dark palette
toggleChange(event: CustomEvent) {
this.toggleDarkPalette(event.detail.checked);
this.initializeDarkPalette(event.detail.checked);
}

// Add or remove the "ion-palette-dark" class on the html element
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
<ion-list-header>Appearance</ion-list-header>
<ion-list [inset]="true">
<ion-item>
<ion-toggle [(ngModel)]="darkPaletteToggle" (ionChange)="darkPaletteToggleChange($event)" justify="space-between"
<ion-toggle [checked]="darkPaletteToggle()" (ionChange)="darkPaletteToggleChange($event)" justify="space-between"
>Dark Mode</ion-toggle
>
</ion-item>
<ion-item>
<ion-toggle
[(ngModel)]="highContrastPaletteToggle"
[checked]="highContrastPaletteToggle()"
(ionChange)="highContrastPaletteToggleChange($event)"
justify="space-between"
>High Contrast Mode</ion-toggle
Expand Down
Loading
Loading