diff --git a/docs/angular/your-first-app.md b/docs/angular/your-first-app.md index 33da48ce580..896e0de6c72 100644 --- a/docs/angular/your-first-app.md +++ b/docs/angular/your-first-app.md @@ -78,7 +78,7 @@ ionic start photo-gallery tabs --type=angular :::note -When prompted to choose between `NgModules` and `Standalone`, opt for `NgModules` as this tutorial follows the `NgModules` approach. +When prompted to choose between `NgModules` and `Standalone`, choose `Standalone` as this tutorial follows the standalone components approach. ::: @@ -109,17 +109,25 @@ npm install @ionic/pwa-elements Next, import `@ionic/pwa-elements` by editing `src/main.ts`. ```ts -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppModule } from './app/app.module'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { RouteReuseStrategy, provideRouter, withPreloading, PreloadAllModules } from '@angular/router'; +import { IonicRouteStrategy, provideIonicAngular } from '@ionic/angular'; // CHANGE: Add the following import import { defineCustomElements } from '@ionic/pwa-elements/loader'; -// CHANGE: Call the element loader before the `bootstrapModule` call +import { routes } from './app/app.routes'; +import { AppComponent } from './app/app.component'; + +// CHANGE: Call the element loader before the `bootstrapApplication` call defineCustomElements(window); -platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch((err) => console.log(err)); +bootstrapApplication(AppComponent, { + providers: [ + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + provideIonicAngular(), + provideRouter(routes, withPreloading(PreloadAllModules)), + ], +}).catch((err) => console.error(err)); ``` That’s it! Now for the fun part - let’s see the app in action. diff --git a/docs/angular/your-first-app/2-taking-photos.md b/docs/angular/your-first-app/2-taking-photos.md index 5d3b3dec862..f71c9de77d1 100644 --- a/docs/angular/your-first-app/2-taking-photos.md +++ b/docs/angular/your-first-app/2-taking-photos.md @@ -61,7 +61,24 @@ Notice the magic here: there's no platform-specific code (web, iOS, or Android)! Next, in `tab2.page.ts`, import the `PhotoService` class and add a method to call its `addNewToGallery` method. ```ts -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; +// CHANGE: Import the Ionic standalone components used on this page +import { + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonGrid, + IonRow, + IonCol, + IonImg, + IonFab, + IonFabButton, + IonIcon, +} from '@ionic/angular'; +// CHANGE: Register the camera icon used by the FAB +import { addIcons } from 'ionicons'; +import { camera } from 'ionicons/icons'; // CHANGE: Import the PhotoService import { PhotoService } from '../services/photo.service'; @@ -69,19 +86,41 @@ import { PhotoService } from '../services/photo.service'; selector: 'app-tab2', templateUrl: 'tab2.page.html', styleUrls: ['tab2.page.scss'], - standalone: false, + // CHANGE: Add the standalone component imports + imports: [ + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonGrid, + IonRow, + IonCol, + IonImg, + IonFab, + IonFabButton, + IonIcon, + ], }) export class Tab2Page { - // CHANGE: Update constructor to include `photoService` - constructor(public photoService: PhotoService) {} + // CHANGE: Inject the PhotoService + public photoService = inject(PhotoService); - // CHANGE: Add `addNewToGallery()` method + constructor() { + // CHANGE: Register the icons this page uses + addIcons({ camera }); + } + + // CHANGE: Add `addPhotoToGallery()` method addPhotoToGallery() { this.photoService.addNewToGallery(); } } ``` +:::note +In a standalone app there is no global icon registry, so each icon you reference by name (like `camera`) must be registered with `addIcons`. Import the specific Ionic components a page uses from `@ionic/angular` and list them in the component's `imports` array. +::: + Then, open `tab2.page.html` and call the `addPhotoToGallery()` method when the FAB is tapped/clicked: ```html @@ -131,12 +170,12 @@ export interface UserPhoto { } ``` -Above the `addNewToGallery()` method, define an array of `UserPhoto`, which will contain a reference to each photo captured with the Camera. +Above the `addNewToGallery()` method, define a [signal](https://angular.dev/guide/signals) that holds an array of `UserPhoto`, which will contain a reference to each photo captured with the Camera. A signal is used so that the gallery view updates automatically when photos change - important in a zoneless app, where mutating a plain array would not trigger a re-render. ```ts export class PhotoService { - // CHANGE: Add the `photos` array - public photos: UserPhoto[] = []; + // CHANGE: Add the `photos` signal + public photos = signal([]); public async addNewToGallery() { // ...existing code... @@ -144,7 +183,7 @@ export class PhotoService { } ``` -Over in the `addNewToGallery` method, add the newly captured photo to the beginning of the `photos` array. +Over in the `addNewToGallery` method, add the newly captured photo to the beginning of the `photos` signal. Reading and updating a signal is done by calling it: `this.photos()` returns the current value, and `this.photos.update()` sets a new one. ```ts // CHANGE: Update `addNewToGallery()` method @@ -156,25 +195,28 @@ public async addNewToGallery() { quality: 100 }); - // CHANGE: Add the new photo to the photos array - this.photos.unshift({ - filepath: "soon...", - webviewPath: capturedPhoto.webPath! - }); + // CHANGE: Add the new photo to the front of the photos signal + this.photos.update((photos) => [ + { + filepath: 'soon...', + webviewPath: capturedPhoto.webPath!, + }, + ...photos, + ]); } ``` `photo.service.ts` should now look like this: ```ts -import { Injectable } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; @Injectable({ providedIn: 'root', }) export class PhotoService { - public photos: UserPhoto[] = []; + public photos = signal([]); public async addNewToGallery() { // Take a photo @@ -184,10 +226,13 @@ export class PhotoService { quality: 100, }); - this.photos.unshift({ - filepath: 'soon...', - webviewPath: capturedPhoto.webPath!, - }); + this.photos.update((photos) => [ + { + filepath: 'soon...', + webviewPath: capturedPhoto.webPath!, + }, + ...photos, + ]); } } @@ -197,7 +242,7 @@ export interface UserPhoto { } ``` -Next, switch to `tab2.page.html` to display the images. We'll add a [Grid component](../../api/grid.md) to ensure the photos display neatly as they're added to the gallery. Inside the grid, loop through each photo in the `PhotoService`'s `photos` array. For each item, add an [Image component](../../api/img.md) and set its `src` property to the photo's path. +Next, switch to `tab2.page.html` to display the images. We'll add a [Grid component](../../api/grid.md) so the photos display neatly as they're added to the gallery. Inside the grid, loop through each photo in the `PhotoService`'s `photos` signal with the built-in [`@for`](https://angular.dev/guide/templates/control-flow#for-block-repeaters) block - calling `photoService.photos()` reads the signal's current value. For each item, add an [Image component](../../api/img.md) and set its `src` property to the photo's path. ```html @@ -217,9 +262,11 @@ Next, switch to `tab2.page.html` to display the images. We'll add a [Grid compon - + @for (photo of photoService.photos(); track photo.webviewPath; let position = $index) { + + } diff --git a/docs/angular/your-first-app/3-saving-photos.md b/docs/angular/your-first-app/3-saving-photos.md index 4212757730d..2986081b7f4 100644 --- a/docs/angular/your-first-app/3-saving-photos.md +++ b/docs/angular/your-first-app/3-saving-photos.md @@ -46,14 +46,14 @@ export interface UserPhoto { We can use this new method immediately in `addNewToGallery()`. ```ts -import { Injectable } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; @Injectable({ providedIn: 'root', }) export class PhotoService { - public photos: UserPhoto[] = []; + public photos = signal([]); // CHANGE: Update the `addNewToGallery()` method public async addNewToGallery() { @@ -68,8 +68,8 @@ export class PhotoService { // Save the picture and add it to photo collection const savedImageFile = await this.savePicture(capturedPhoto); - // CHANGE: Update argument to unshift array method - this.photos.unshift(savedImageFile); + // CHANGE: Add the saved photo to the front of the photos signal + this.photos.update((photos) => [savedImageFile, ...photos]); } private async savePicture(photo: Photo) { @@ -150,7 +150,7 @@ export interface UserPhoto { `photo.service.ts` should now look like this: ```ts -import { Injectable } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; @@ -159,7 +159,7 @@ import { Filesystem, Directory } from '@capacitor/filesystem'; providedIn: 'root', }) export class PhotoService { - public photos: UserPhoto[] = []; + public photos = signal([]); public async addNewToGallery() { // Take a photo @@ -172,7 +172,7 @@ export class PhotoService { // Save the picture and add it to photo collection const savedImageFile = await this.savePicture(capturedPhoto); - this.photos.unshift(savedImageFile); + this.photos.update((photos) => [savedImageFile, ...photos]); } private async savePicture(photo: Photo) { diff --git a/docs/angular/your-first-app/4-loading-photos.md b/docs/angular/your-first-app/4-loading-photos.md index db1919122c6..e9864fbd5b9 100644 --- a/docs/angular/your-first-app/4-loading-photos.md +++ b/docs/angular/your-first-app/4-loading-photos.md @@ -21,7 +21,7 @@ Open `photo.service.ts` and begin by defining a new property in the `PhotoServic ```ts export class PhotoService { - public photos: UserPhoto[] = []; + public photos = signal([]); // CHANGE: Add a key for photo storage private PHOTO_STORAGE: string = 'photos'; @@ -57,12 +57,12 @@ export class PhotoService { const savedImageFile = await this.savePicture(capturedPhoto); - this.photos.unshift(savedImageFile); + this.photos.update((photos) => [savedImageFile, ...photos]); // CHANGE: Add method to cache all photo data for future retrieval Preferences.set({ key: this.PHOTO_STORAGE, - value: JSON.stringify(this.photos), + value: JSON.stringify(this.photos()), }); } @@ -85,7 +85,7 @@ export class PhotoService { public async loadSaved() { // Retrieve cached photo array data const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); - this.photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + this.photos.set((photoList ? JSON.parse(photoList) : []) as UserPhoto[]); } } ``` @@ -100,10 +100,10 @@ export class PhotoService { public async loadSaved() { // Retrieve cached photo array data const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); - this.photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; // CHANGE: Display the photo by reading into base64 format - for (let photo of this.photos) { + for (const photo of photos) { // Read each saved photo's data from the Filesystem const readFile = await Filesystem.readFile({ path: photo.filepath, @@ -113,6 +113,9 @@ export class PhotoService { // Web platform only: Load the photo as base64 data photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`; } + + // CHANGE: Set the signal so the gallery view updates + this.photos.set(photos); } } ``` @@ -120,7 +123,7 @@ export class PhotoService { `photo.service.ts` should now look like this: ```ts -import { Injectable } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; @@ -130,7 +133,7 @@ import { Preferences } from '@capacitor/preferences'; providedIn: 'root', }) export class PhotoService { - public photos: UserPhoto[] = []; + public photos = signal([]); private PHOTO_STORAGE: string = 'photos'; @@ -145,11 +148,11 @@ export class PhotoService { // Save the picture and add it to photo collection const savedImageFile = await this.savePicture(capturedPhoto); - this.photos.unshift(savedImageFile); + this.photos.update((photos) => [savedImageFile, ...photos]); Preferences.set({ key: this.PHOTO_STORAGE, - value: JSON.stringify(this.photos), + value: JSON.stringify(this.photos()), }); } @@ -189,9 +192,9 @@ export class PhotoService { public async loadSaved() { // Retrieve cached photo array data const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); - this.photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; - for (let photo of this.photos) { + for (const photo of photos) { // Read each saved photo's data from the Filesystem const readFile = await Filesystem.readFile({ path: photo.filepath, @@ -201,6 +204,8 @@ export class PhotoService { // Web platform only: Load the photo as base64 data photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`; } + + this.photos.set(photos); } } @@ -215,17 +220,48 @@ Our `PhotoService` can now load the saved images, but we'll need to update `tab2 Update `tab2.page.ts` to look like the following: ```ts -import { Component } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; +import { + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonGrid, + IonRow, + IonCol, + IonImg, + IonFab, + IonFabButton, + IonIcon, +} from '@ionic/angular'; +import { addIcons } from 'ionicons'; +import { camera } from 'ionicons/icons'; import { PhotoService } from '../services/photo.service'; @Component({ selector: 'app-tab2', templateUrl: 'tab2.page.html', styleUrls: ['tab2.page.scss'], - standalone: false, + imports: [ + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonGrid, + IonRow, + IonCol, + IonImg, + IonFab, + IonFabButton, + IonIcon, + ], }) -export class Tab2Page { - constructor(public photoService: PhotoService) {} +export class Tab2Page implements OnInit { + public photoService = inject(PhotoService); + + constructor() { + addIcons({ camera }); + } // CHANGE: Add call to `loadSaved()` when navigating to the Photos tab async ngOnInit() { diff --git a/docs/angular/your-first-app/5-adding-mobile.md b/docs/angular/your-first-app/5-adding-mobile.md index aee2ab86be9..ce7c3480d17 100644 --- a/docs/angular/your-first-app/5-adding-mobile.md +++ b/docs/angular/your-first-app/5-adding-mobile.md @@ -22,7 +22,7 @@ Import the Ionic [Platform API](../platform.md) into `photo.service.ts`, which i Add `Platform` to the imports at the top of the file and a new property `platform` to the `PhotoService` class. We'll also need to update the constructor to set the user's platform. ```ts -import { Injectable } from '@angular/core'; +import { Injectable, inject, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; @@ -34,17 +34,12 @@ import { Platform } from '@ionic/angular'; providedIn: 'root', }) export class PhotoService { - public photos: UserPhoto[] = []; + public photos = signal([]); private PHOTO_STORAGE: string = 'photos'; - // CHANGE: Add a property to track the app's running platform - private platform: Platform; - - // CHANGE: Update constructor to set `platform` - constructor(platform: Platform) { - this.platform = platform; - } + // CHANGE: Inject the Platform API to track the app's running platform + private platform = inject(Platform); // ...existing code... } @@ -96,7 +91,7 @@ private async savePicture(photo: Photo) { When running on mobile, set `filepath` to the result of the `writeFile()` operation - `savedFile.uri`. When setting the `webviewPath`, use the special `Capacitor.convertFileSrc()` method ([details on the File Protocol](../../core-concepts/webview.md#file-protocol)). To use this method, we'll need to import Capacitor into `photo.service.ts`. ```ts -import { Injectable } from '@angular/core'; +import { Injectable, inject, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; @@ -159,12 +154,12 @@ Next, add a new bit of logic in the `loadSaved()` method. On mobile, we can dire // CHANGE: Update `loadSaved()` method public async loadSaved() { const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); - this.photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; // CHANGE: Add platform check // If running on the web... if (!this.platform.is('hybrid')) { - for (let photo of this.photos) { + for (const photo of photos) { const readFile = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data @@ -174,6 +169,9 @@ public async loadSaved() { photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`; } } + + // CHANGE: Set the signal so the gallery view updates + this.photos.set(photos); } ``` @@ -182,7 +180,7 @@ Our Photo Gallery now consists of one codebase that runs on the web, Android, an `photos.service.ts` should now look like this: ```ts -import { Injectable } from '@angular/core'; +import { Injectable, inject, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; @@ -194,15 +192,11 @@ import { Capacitor } from '@capacitor/core'; providedIn: 'root', }) export class PhotoService { - public photos: UserPhoto[] = []; + public photos = signal([]); private PHOTO_STORAGE: string = 'photos'; - private platform: Platform; - - constructor(platform: Platform) { - this.platform = platform; - } + private platform = inject(Platform); public async addNewToGallery() { // Take a photo @@ -214,11 +208,11 @@ export class PhotoService { const savedImageFile = await this.savePicture(capturedPhoto); - this.photos.unshift(savedImageFile); + this.photos.update((photos) => [savedImageFile, ...photos]); Preferences.set({ key: this.PHOTO_STORAGE, - value: JSON.stringify(this.photos), + value: JSON.stringify(this.photos()), }); } @@ -279,11 +273,11 @@ export class PhotoService { public async loadSaved() { // Retrieve cached photo array data const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); - this.photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; // If running on the web... if (!this.platform.is('hybrid')) { - for (let photo of this.photos) { + for (const photo of photos) { const readFile = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data, @@ -292,6 +286,8 @@ export class PhotoService { photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`; } } + + this.photos.set(photos); } } diff --git a/docs/angular/your-first-app/7-live-reload.md b/docs/angular/your-first-app/7-live-reload.md index 96b58ced5ef..5f51fe1f3b0 100644 --- a/docs/angular/your-first-app/7-live-reload.md +++ b/docs/angular/your-first-app/7-live-reload.md @@ -38,7 +38,7 @@ With Live Reload running and the app open on your device, let’s implement phot In `photo.service.ts`, add the `deletePhoto()` method. The selected photo is removed from the `photos` array first. Then, we use the Capacitor Preferences API to update the cached version of the `photos` array. Finally, we delete the actual photo file itself using the Filesystem API. ```ts -import { Injectable } from '@angular/core'; +import { Injectable, inject, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; @@ -54,13 +54,13 @@ export class PhotoService { // CHANGE: Add `deletePhoto()` method public async deletePhoto(photo: UserPhoto, position: number) { - // Remove this photo from the Photos reference data array - this.photos.splice(position, 1); + // Remove this photo from the photos signal + this.photos.update((photos) => photos.filter((_, index) => index !== position)); // Update photos array cache by overwriting the existing photo array Preferences.set({ key: this.PHOTO_STORAGE, - value: JSON.stringify(this.photos), + value: JSON.stringify(this.photos()), }); // Delete photo file from filesystem @@ -82,24 +82,64 @@ export interface UserPhoto { Next, in `tab2.page.ts`, implement the `showActionSheet()` method. We're adding two options: "Delete", which calls `PhotoService.deletePhoto()`, and "Cancel". The cancel button will automatically close the action sheet when assigned the "cancel" role. ```ts -import { Component } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; +import { + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonGrid, + IonRow, + IonCol, + IonImg, + IonFab, + IonFabButton, + IonIcon, + // CHANGE: Add import + ActionSheetController, +} from '@ionic/angular'; +import { addIcons } from 'ionicons'; +// CHANGE: Register the `trash` and `close` icons used by the action sheet +import { camera, trash, close } from 'ionicons/icons'; // Change: Add import import type { UserPhoto } from '../services/photo.service'; import { PhotoService } from '../services/photo.service'; -// CHANGE: Add import -import { ActionSheetController } from '@ionic/angular'; @Component({ selector: 'app-tab2', templateUrl: 'tab2.page.html', styleUrls: ['tab2.page.scss'], - standalone: false, + imports: [ + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonGrid, + IonRow, + IonCol, + IonImg, + IonFab, + IonFabButton, + IonIcon, + ], }) -export class Tab2Page { - // CHANGE: Update constructor - constructor(public photoService: PhotoService, public actionSheetController: ActionSheetController) {} +export class Tab2Page implements OnInit { + public photoService = inject(PhotoService); + // CHANGE: Inject the ActionSheetController + private actionSheetController = inject(ActionSheetController); + + constructor() { + // CHANGE: Register the icons this page uses + addIcons({ camera, trash, close }); + } - // ...existing code... + async ngOnInit() { + await this.photoService.loadSaved(); + } + + addPhotoToGallery() { + this.photoService.addNewToGallery(); + } // CHANGE: Add `showActionSheet()` method public async showActionSheet(photo: UserPhoto, position: number) { @@ -147,10 +187,12 @@ Open `tab2.page.html` and add a new click handler to each `` element. W - + @for (photo of photoService.photos(); track photo.webviewPath; let position = $index) { + + }