From 2dc4159b75e7367f9b412caa9616164d40e54add Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:38:41 -0400 Subject: [PATCH 1/5] docs: align tutorial, quickstart, and testing pages with the updated starters --- docs/angular/quickstart.mdx | 4 +- docs/angular/your-first-app.mdx | 6 +- .../your-first-app/2-taking-photos.mdx | 13 ++-- .../your-first-app/3-saving-photos.mdx | 9 +-- .../your-first-app/4-loading-photos.mdx | 30 +++++---- .../your-first-app/5-adding-mobile.mdx | 34 +++++----- docs/angular/your-first-app/7-live-reload.mdx | 16 +++-- docs/react/quickstart.mdx | 20 +++++- docs/react/testing/unit-testing/setup.mdx | 46 +++++++++---- docs/react/your-first-app.mdx | 15 +++-- docs/react/your-first-app/2-taking-photos.mdx | 34 +++++++--- docs/react/your-first-app/3-saving-photos.mdx | 36 ++++++---- .../react/your-first-app/4-loading-photos.mdx | 39 +++++++---- docs/react/your-first-app/5-adding-mobile.mdx | 19 ++++-- docs/react/your-first-app/7-live-reload.mdx | 24 ++++--- docs/vue/quickstart.mdx | 19 +++--- docs/vue/testing.mdx | 5 +- docs/vue/troubleshooting.mdx | 13 ++-- docs/vue/your-first-app.mdx | 8 +-- docs/vue/your-first-app/2-taking-photos.mdx | 33 ++++++--- docs/vue/your-first-app/3-saving-photos.mdx | 40 +++++++---- docs/vue/your-first-app/4-loading-photos.mdx | 67 +++++++++++++------ docs/vue/your-first-app/5-adding-mobile.mdx | 32 +++++---- docs/vue/your-first-app/7-live-reload.mdx | 23 +++++-- 24 files changed, 380 insertions(+), 205 deletions(-) diff --git a/docs/angular/quickstart.mdx b/docs/angular/quickstart.mdx index e043b556dd..46d9e8f73c 100644 --- a/docs/angular/quickstart.mdx +++ b/docs/angular/quickstart.mdx @@ -112,7 +112,7 @@ This sets up the root of your application, using Ionic's `ion-app` and `ion-rout Routes are defined in `app.routes.ts`: ```ts title="src/app/app.routes.ts" -import { Routes } from '@angular/router'; +import type { Routes } from '@angular/router'; export const routes: Routes = [ { @@ -382,7 +382,7 @@ export class NewPage implements OnInit { ngOnInit() {} - scrollToBottom = () => { + scrollToBottom = (): void => { this.content.scrollToBottom(300); }; } diff --git a/docs/angular/your-first-app.mdx b/docs/angular/your-first-app.mdx index 7db2aaac48..281ab9f3cd 100644 --- a/docs/angular/your-first-app.mdx +++ b/docs/angular/your-first-app.mdx @@ -115,8 +115,8 @@ import { IonicRouteStrategy, provideIonicAngular } from '@ionic/angular'; // CHANGE: Add the following import import { defineCustomElements } from '@ionic/pwa-elements/loader'; -import { routes } from './app/app.routes'; import { AppComponent } from './app/app.component'; +import { routes } from './app/app.routes'; // CHANGE: Call the element loader before the `bootstrapApplication` call defineCustomElements(window); @@ -195,11 +195,11 @@ We put the visual aspects of our app into ``. In this case, it’s ```html - Photo Gallery + Photo Gallery - + Photo Gallery diff --git a/docs/angular/your-first-app/2-taking-photos.mdx b/docs/angular/your-first-app/2-taking-photos.mdx index 898e2d6fc9..ed219746a3 100644 --- a/docs/angular/your-first-app/2-taking-photos.mdx +++ b/docs/angular/your-first-app/2-taking-photos.mdx @@ -45,7 +45,7 @@ import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; }) export class PhotoService { // CHANGE: Add the gallery method - public async addNewToGallery() { + public async addNewToGallery(): Promise { // Take a photo const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, @@ -78,6 +78,7 @@ import { // 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'; @@ -98,7 +99,7 @@ export class Tab2Page { } // CHANGE: Add `addPhotoToGallery()` method - addPhotoToGallery() { + addPhotoToGallery(): void { this.photoService.addNewToGallery(); } } @@ -166,7 +167,7 @@ export class PhotoService { // CHANGE: Add the `photos` signal public photos = signal([]); - public async addNewToGallery() { + public async addNewToGallery(): Promise { // ...existing code... } } @@ -176,12 +177,12 @@ Over in the `addNewToGallery` method, add the newly captured photo to the beginn ```ts // CHANGE: Update `addNewToGallery()` method -public async addNewToGallery() { +public async addNewToGallery(): Promise { // Take a photo const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, source: CameraSource.Camera, - quality: 100 + quality: 100, }); // CHANGE: Add the new photo to the front of the photos signal @@ -207,7 +208,7 @@ import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; export class PhotoService { public photos = signal([]); - public async addNewToGallery() { + public async addNewToGallery(): Promise { // Take a photo const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, diff --git a/docs/angular/your-first-app/3-saving-photos.mdx b/docs/angular/your-first-app/3-saving-photos.mdx index 5529c6dbd2..edfe8dfdae 100644 --- a/docs/angular/your-first-app/3-saving-photos.mdx +++ b/docs/angular/your-first-app/3-saving-photos.mdx @@ -47,7 +47,8 @@ We can use this new method immediately in `addNewToGallery()`. ```ts import { Injectable, signal } from '@angular/core'; -import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; @Injectable({ providedIn: 'root', @@ -56,7 +57,7 @@ export class PhotoService { public photos = signal([]); // CHANGE: Update the `addNewToGallery()` method - public async addNewToGallery() { + public async addNewToGallery(): Promise { // Take a photo const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, @@ -93,7 +94,7 @@ Then, pass the data to the Filesystem's `writeFile` method. Recall that we displ Create a new helper method, `base64FromPath()`, to implement the necessary logic for running on the web: ```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'; // CHANGE: Add import @@ -164,7 +165,7 @@ import { Filesystem, Directory } from '@capacitor/filesystem'; export class PhotoService { public photos = signal([]); - public async addNewToGallery() { + public async addNewToGallery(): Promise { // Take a photo const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, diff --git a/docs/angular/your-first-app/4-loading-photos.mdx b/docs/angular/your-first-app/4-loading-photos.mdx index 5a5b5fc626..100366073e 100644 --- a/docs/angular/your-first-app/4-loading-photos.mdx +++ b/docs/angular/your-first-app/4-loading-photos.mdx @@ -33,7 +33,7 @@ export class PhotoService { Next, at the end of the `addNewToGallery()` method, add a call to `Preferences.set()` to save the `photos` array. By adding it here, the `photos` array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. ```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'; @@ -47,7 +47,7 @@ export class PhotoService { // ...existing code... // CHANGE: Update `addNewToGallery()` method - public async addNewToGallery() { + public async addNewToGallery(): Promise { // Take a photo const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, @@ -59,7 +59,7 @@ export class PhotoService { this.photos.update((photos) => [savedImageFile, ...photos]); - // CHANGE: Add method to cache all photo data for future retrieval + // CHANGE: Cache all photo data for future retrieval Preferences.set({ key: this.PHOTO_STORAGE, value: JSON.stringify(this.photos()), @@ -82,7 +82,7 @@ export class PhotoService { // ...existing code... // CHANGE: Add the method to load the photo data - public async loadSaved() { + public async loadSaved(): Promise { // Retrieve cached photo array data const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); this.photos.set((photoList ? JSON.parse(photoList) : []) as UserPhoto[]); @@ -97,7 +97,7 @@ export class PhotoService { // ...existing code... // CHANGE: Update the `loadSaved()` method - public async loadSaved() { + public async loadSaved(): Promise { // Retrieve cached photo array data const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; @@ -105,13 +105,13 @@ export class PhotoService { // CHANGE: Display the photo by reading into base64 format for (const photo of photos) { // Read each saved photo's data from the Filesystem - const file = await Filesystem.readFile({ + const readFile = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data, }); // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`; } // CHANGE: Set the signal so the gallery view updates @@ -137,7 +137,7 @@ export class PhotoService { private PHOTO_STORAGE: string = 'photos'; - public async addNewToGallery() { + public async addNewToGallery(): Promise { // Take a photo const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, @@ -192,20 +192,20 @@ export class PhotoService { }); } - public async loadSaved() { + public async loadSaved(): Promise { // Retrieve cached photo array data const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; for (const photo of photos) { // Read each saved photo's data from the Filesystem - const file = await Filesystem.readFile({ + const readFile = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data, }); // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`; } this.photos.set(photos); @@ -223,7 +223,8 @@ 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, OnInit, inject } from '@angular/core'; +import type { OnInit } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { IonHeader, IonToolbar, @@ -238,6 +239,7 @@ import { } from '@ionic/angular'; import { addIcons } from 'ionicons'; import { camera } from 'ionicons/icons'; + import { PhotoService } from '../services/photo.service'; @Component({ @@ -254,11 +256,11 @@ export class Tab2Page implements OnInit { } // CHANGE: Add call to `loadSaved()` when navigating to the Photos tab - async ngOnInit() { + async ngOnInit(): Promise { await this.photoService.loadSaved(); } - addPhotoToGallery() { + addPhotoToGallery(): void { this.photoService.addNewToGallery(); } } diff --git a/docs/angular/your-first-app/5-adding-mobile.mdx b/docs/angular/your-first-app/5-adding-mobile.mdx index 0f3ce3eb08..5cd2729cb9 100644 --- a/docs/angular/your-first-app/5-adding-mobile.mdx +++ b/docs/angular/your-first-app/5-adding-mobile.mdx @@ -61,8 +61,9 @@ private async savePicture(photo: Photo) { if (this.platform.is('hybrid')) { // Read the file into base64 format const file = await Filesystem.readFile({ - path: photo.path! + path: photo.path!, }); + base64Data = file.data; } else { base64Data = await this.base64FromPath(photo.webPath!); @@ -73,7 +74,7 @@ private async savePicture(photo: Photo) { const savedFile = await Filesystem.writeFile({ path: fileName, data: base64Data, - directory: Directory.Data + directory: Directory.Data, }); // Use webPath to display the new image instead of base64 since it's @@ -91,11 +92,11 @@ When running on mobile, set `filepath` to the result of the `writeFile()` operat import { Injectable, inject, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +// CHANGE: Add import +import { Capacitor } from '@capacitor/core'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; import { Platform } from '@ionic/angular'; -// Change: Add import -import { Capacitor } from '@capacitor/core'; // ...existing code... ``` @@ -106,11 +107,13 @@ Then update `savePicture()` to look like the following: // CHANGE: Update `savePicture()` method private async savePicture(photo: Photo) { let base64Data: string | Blob; + // "hybrid" will detect mobile - iOS or Android if (this.platform.is('hybrid')) { const file = await Filesystem.readFile({ path: photo.path!, }); + base64Data = file.data; } else { base64Data = await this.base64FromPath(photo.webPath!); @@ -146,7 +149,7 @@ Next, add a new bit of logic in the `loadSaved()` method. On mobile, we can dire ```ts // CHANGE: Update `loadSaved()` method -public async loadSaved() { +public async loadSaved(): Promise { const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; @@ -154,13 +157,13 @@ public async loadSaved() { // If running on the web... if (!this.platform.is('hybrid')) { for (const photo of photos) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data + const readFile = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, }); // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`; } } @@ -171,16 +174,16 @@ public async loadSaved() { Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. -`photos.service.ts` should now look like this: +`photo.service.ts` should now look like this: ```ts import { Injectable, inject, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Capacitor } from '@capacitor/core'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; import { Platform } from '@ionic/angular'; -import { Capacitor } from '@capacitor/core'; @Injectable({ providedIn: 'root', @@ -192,7 +195,7 @@ export class PhotoService { private platform = inject(Platform); - public async addNewToGallery() { + public async addNewToGallery(): Promise { // Take a photo const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, @@ -266,7 +269,7 @@ export class PhotoService { }); } - public async loadSaved() { + public async loadSaved(): Promise { // Retrieve cached photo array data const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; @@ -274,12 +277,13 @@ export class PhotoService { // If running on the web... if (!this.platform.is('hybrid')) { for (const photo of photos) { - const file = await Filesystem.readFile({ + const readFile = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data, }); + // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + photo.webviewPath = `data:image/jpeg;base64,${readFile.data}`; } } diff --git a/docs/angular/your-first-app/7-live-reload.mdx b/docs/angular/your-first-app/7-live-reload.mdx index 74dac3b0cf..9473e6e9cc 100644 --- a/docs/angular/your-first-app/7-live-reload.mdx +++ b/docs/angular/your-first-app/7-live-reload.mdx @@ -41,10 +41,10 @@ In `photo.service.ts`, add the `deletePhoto()` method. The selected photo is rem import { Injectable, inject, signal } from '@angular/core'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Capacitor } from '@capacitor/core'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; import { Platform } from '@ionic/angular'; -import { Capacitor } from '@capacitor/core'; @Injectable({ providedIn: 'root', @@ -53,7 +53,7 @@ export class PhotoService { // ...existing code... // CHANGE: Add `deletePhoto()` method - public async deletePhoto(photo: UserPhoto, position: number) { + public async deletePhoto(photo: UserPhoto, position: number): Promise { // Remove this photo from the photos signal this.photos.update((photos) => photos.filter((_, index) => index !== position)); @@ -82,7 +82,8 @@ 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, OnInit, inject } from '@angular/core'; +import type { OnInit } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { IonHeader, IonToolbar, @@ -100,7 +101,8 @@ import { 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 + +// CHANGE: Add import import type { UserPhoto } from '../services/photo.service'; import { PhotoService } from '../services/photo.service'; @@ -120,16 +122,16 @@ export class Tab2Page implements OnInit { addIcons({ camera, trash, close }); } - async ngOnInit() { + async ngOnInit(): Promise { await this.photoService.loadSaved(); } - addPhotoToGallery() { + addPhotoToGallery(): void { this.photoService.addNewToGallery(); } // CHANGE: Add `showActionSheet()` method - public async showActionSheet(photo: UserPhoto, position: number) { + public async showActionSheet(photo: UserPhoto, position: number): Promise { const actionSheet = await this.actionSheetController.create({ header: 'Photos', buttons: [ diff --git a/docs/react/quickstart.mdx b/docs/react/quickstart.mdx index 972203a096..29263b0287 100644 --- a/docs/react/quickstart.mdx +++ b/docs/react/quickstart.mdx @@ -80,9 +80,10 @@ Let's walk through these files to understand the app's structure. The root of your app is defined in `App.tsx`: ```tsx title="src/App.tsx" -import { Route, Navigate } from 'react-router-dom'; import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react'; import { IonReactRouter } from '@ionic/react-router'; +import { Navigate, Route } from 'react-router-dom'; + import Home from './pages/Home'; // ..CSS imports... @@ -124,6 +125,7 @@ The Home page component, defined in `Home.tsx`, imports the Ionic components and ```tsx title="src/pages/Home.tsx" import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; + import ExploreContainer from '../components/ExploreContainer'; import './Home.css'; @@ -303,9 +305,21 @@ Update `New.tsx` to add a `ref` on `IonContent` and a button and some items afte Then, add the imports for the additional components and define the `scrollToBottom` function: ```tsx title="src/pages/New.tsx" -import { useRef } from 'react'; -import { IonButton, IonBackButton, IonButtons, IonContent, IonHeader, IonIcon, IonItem, IonLabel, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import { + IonButton, + IonBackButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, + IonItem, + IonLabel, + IonPage, + IonTitle, + IonToolbar, +} from '@ionic/react'; import { heart, logoIonic } from 'ionicons/icons'; +import { useRef } from 'react'; const New: React.FC = () => { const content = useRef(null); diff --git a/docs/react/testing/unit-testing/setup.mdx b/docs/react/testing/unit-testing/setup.mdx index aae81f5703..ad0d5f1f4b 100644 --- a/docs/react/testing/unit-testing/setup.mdx +++ b/docs/react/testing/unit-testing/setup.mdx @@ -6,35 +6,55 @@ description: Learn how to set up unit tests for an Ionic React application. # Unit Testing Setup -Ionic requires a few additional steps to set up unit tests. If you are using an Ionic starter project, these steps have already been completed for you. +Ionic requires a few additional steps to set up unit tests. If you are using an Ionic starter project, React Testing Library and jest-dom are already installed and `src/setupTests.ts` is already configured; you only need to initialize Ionic React as described below. ### Install React Testing Library {/* #install-react-testing-library */} React Testing Library is a set of utilities that make it easier to test React components. It's used to interact with components and test their behavior. ```bash -npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event +npm install --save-dev @testing-library/react @testing-library/dom @testing-library/jest-dom +``` + +To simulate user interactions with [`user-event`](./best-practices.mdx#use-user-event-for-user-interactions), install it as well. It is not included in Ionic starter projects: + +```bash +npm install --save-dev @testing-library/user-event ``` ### Initialize Ionic React {/* #initialize-ionic-react */} Ionic React requires the `setupIonicReact` function to be called before any tests are run. Failing to do so will result in mode-based classes and platform behaviors not being applied to your components. -In `src/setupTest.ts`, add the following code: +In `src/setupTests.ts`, add the following code: ```diff -import '@testing-library/jest-dom/extend-expect'; - +// Adds jest-dom's DOM matchers (toBeInTheDocument, toHaveTextContent, ...) to Vitest's expect. +import '@testing-library/jest-dom/vitest'; + import { setupIonicReact } from '@ionic/react'; +import { cleanup } from '@testing-library/react'; +import { afterEach } from 'vitest'; + setupIonicReact(); - -// Mock matchmedia -window.matchMedia = window.matchMedia || function () { - return { - matches: false, - addListener: function () { }, - removeListener: function () { } ++ +// Testing Library only cleans up automatically when Vitest globals are enabled. +afterEach(() => { + cleanup(); +}); + +// jsdom doesn't implement matchMedia, which Ionic uses for platform and dark mode checks. +window.matchMedia = + window.matchMedia || + function (query: string): MediaQueryList { + return { + matches: false, + media: query, + onchange: null, + addListener: () => undefined, + removeListener: () => undefined, + addEventListener: () => undefined, + removeEventListener: () => undefined, + dispatchEvent: () => false, + }; }; -}; ``` diff --git a/docs/react/your-first-app.mdx b/docs/react/your-first-app.mdx index fa052a476d..0ff8fcdef0 100644 --- a/docs/react/your-first-app.mdx +++ b/docs/react/your-first-app.mdx @@ -103,15 +103,15 @@ npm install @ionic/pwa-elements Next, import `@ionic/pwa-elements` by editing `src/main.tsx`. ```tsx +// CHANGE: Add the following import +import { defineCustomElements } from '@ionic/pwa-elements/loader'; import React from 'react'; import { createRoot } from 'react-dom/client'; + import App from './App'; -// CHANGE: Add the following import -import { defineCustomElements } from '@ionic/pwa-elements/loader'; // CHANGE: Call the element loader before the render call defineCustomElements(window); - const container = document.getElementById('root'); const root = createRoot(container!); root.render( @@ -143,6 +143,7 @@ Open `/src/pages/Tab2.tsx`. It contains: ```tsx import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; + import ExploreContainer from '../components/ExploreContainer'; import './Tab2.css'; @@ -195,10 +196,11 @@ export default Tab2; We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](../api/fab.mdx) (FAB) to the bottom of the page and set the camera image as the icon. ```tsx -// CHANGE: Add the following import -import { camera } from 'ionicons/icons'; // CHANGE: Update the following import import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonFab, IonFabButton, IonIcon } from '@ionic/react'; +// CHANGE: Add the following import +import { camera } from 'ionicons/icons'; + // CHANGE: Remove or comment out `ExploreContainer` // import ExploreContainer from '../components/ExploreContainer'; import './Tab2.css'; @@ -238,7 +240,6 @@ export default Tab2; Next, open `src/App.tsx`. Change the label to "Photos" and the `ellipse` icon to `images` for the middle tab button. ```tsx -import { Route, Navigate } from 'react-router-dom'; import { IonApp, IonIcon, @@ -252,6 +253,8 @@ import { import { IonReactRouter } from '@ionic/react-router'; // CHANGE: Update the following import import { images, square, triangle } from 'ionicons/icons'; +import { Navigate, Route } from 'react-router-dom'; + import Tab1 from './pages/Tab1'; import Tab2 from './pages/Tab2'; import Tab3 from './pages/Tab3'; diff --git a/docs/react/your-first-app/2-taking-photos.mdx b/docs/react/your-first-app/2-taking-photos.mdx index b65238b780..15cd85130a 100644 --- a/docs/react/your-first-app/2-taking-photos.mdx +++ b/docs/react/your-first-app/2-taking-photos.mdx @@ -24,7 +24,9 @@ Next, define a new method, `usePhotoGallery()`, that will contain the core logic ```ts import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; +} { const addNewToGallery = async () => { // Take a photo const capturedPhoto = await Camera.getPhoto({ @@ -45,8 +47,9 @@ Notice the magic here: there's no platform-specific code (web, iOS, or Android)! Next, in `Tab2.tsx`, import the `usePhotoGallery()` method and destructure it to call its `addNewToGallery()` method. ```tsx -import { camera } from 'ionicons/icons'; import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonFab, IonFabButton, IonIcon } from '@ionic/react'; +import { camera } from 'ionicons/icons'; + // CHANGE: Add `usePhotoGallery` import import { usePhotoGallery } from '../hooks/usePhotoGallery'; import './Tab2.css'; @@ -96,7 +99,9 @@ After taking a photo, it disappears right away. We need to display it within our To define the data structure for our photo metadata, create a new interface named `UserPhoto`. Add this interface at the very bottom of the `usePhotoGallery.ts` file, immediately after the `usePhotoGallery()` method definition. ```ts -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; +} { // ...existing code... } @@ -110,11 +115,13 @@ export interface UserPhoto { Above the `addNewToGallery()` method, define an array of `UserPhoto`, which will contain a reference to each photo captured with the Camera. Make it a state variable using React's [useState hook](https://react.dev/reference/react/useState). ```ts +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; // CHANGE: Add import import { useState } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; +} { // CHANGE: Add the `photos` array const [photos, setPhotos] = useState([]); @@ -125,7 +132,11 @@ export function usePhotoGallery() { Over in the `addNewToGallery()` method, add the newly captured photo to the beginning of the `photos` array. Then, update the `usePhotoGallery()` return statement with the `photos` array. ```ts -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + // CHANGE: Add `photos` to the return type + photos: UserPhoto[]; +} { const [photos, setPhotos] = useState([]); // CHANGE: Update `addNewToGallery()` method @@ -163,10 +174,13 @@ export function usePhotoGallery() { `usePhotoGallery.ts` should now look like this: ```ts -import { useState } from 'react'; import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { useState } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { const [photos, setPhotos] = useState([]); const addNewToGallery = async () => { @@ -204,7 +218,6 @@ export interface UserPhoto { Next, switch to `Tab2.tsx` to display the images. We'll add a [Grid component](../../api/grid.mdx) to ensure the photos display neatly as they're added to the gallery. Inside the grid, loop through each photo in the `UserPhoto`'s `photos` array. For each item, add an `` element and set its `src` property to the photo's path. ```tsx -import { camera } from 'ionicons/icons'; // CHANGE: Update import import { IonContent, @@ -219,7 +232,10 @@ import { IonRow, IonCol, } from '@ionic/react'; +import { camera } from 'ionicons/icons'; + import { usePhotoGallery } from '../hooks/usePhotoGallery'; +import './Tab2.css'; const Tab2: React.FC = () => { // CHANGE: Add `photos` array to destructure from `usePhotoGallery()` diff --git a/docs/react/your-first-app/3-saving-photos.mdx b/docs/react/your-first-app/3-saving-photos.mdx index 0a88055487..c87288a739 100644 --- a/docs/react/your-first-app/3-saving-photos.mdx +++ b/docs/react/your-first-app/3-saving-photos.mdx @@ -18,12 +18,15 @@ We’re now able to take multiple photos and display them in a photo gallery on Fortunately, saving them to the filesystem only takes a few steps. Begin by creating a new class method, `savePicture()`, in the `usePhotoGallery()` method in `usePhotoGallery.ts`. ```ts -import { useState } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; // CHANGE: Add import import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { useState } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { // ...existing code... // CHANGE: Add the `savePicture()` method @@ -49,11 +52,14 @@ export interface UserPhoto { We can use this new method immediately in `addNewToGallery()`. ```ts -import { useState } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { useState } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { const [photos, setPhotos] = useState([]); const addNewToGallery = async () => { @@ -100,13 +106,16 @@ Then, pass the data to the Filesystem's `writeFile` method. Recall that we displ Create a new helper method, `base64FromPath()`, to implement the necessary logic for running on the web: ```ts -import { useState } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; // CHANGE: Add import import { Filesystem, Directory } from '@capacitor/filesystem'; +import { useState } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { // ...existing code... // CHANGE: Update the `savePicture()` method @@ -160,12 +169,15 @@ export interface UserPhoto { `usePhotoGallery.ts` should now look like this: ```ts -import { useState } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; +import { useState } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { const [photos, setPhotos] = useState([]); const addNewToGallery = async () => { diff --git a/docs/react/your-first-app/4-loading-photos.mdx b/docs/react/your-first-app/4-loading-photos.mdx index 6bd1465a38..3ede73a6e5 100644 --- a/docs/react/your-first-app/4-loading-photos.mdx +++ b/docs/react/your-first-app/4-loading-photos.mdx @@ -20,7 +20,10 @@ Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](/nat Open `usePhotoGallery.ts` and begin by defining a constant variable that will act as the key for the store. ```ts -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { const [photos, setPhotos] = useState([]); // CHANGE: Add a key for photo storage const PHOTO_STORAGE = 'photos'; @@ -32,14 +35,17 @@ export function usePhotoGallery() { Next, at the end of the `addNewToGallery()` method, add a call to the `Preferences.set()` method to save the `photos` array. By adding it here, the `photos` array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. ```ts -import { useState } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; // CHANGE: Add import import { Preferences } from '@capacitor/preferences'; +import { useState } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { // ...existing code... const addNewToGallery = async () => { @@ -61,14 +67,17 @@ export function usePhotoGallery() { With the photo array data saved, create a new method in the `usePhotoGallery()` called `loadSaved()` that can retrieve the photo data. We use the same key to retrieve the `photos` array in JSON format, then parse it into an array. ```ts -// CHANGE: Update import -import { useState, useEffect } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; +// CHANGE: Update import +import { useState, useEffect } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { const [photos, setPhotos] = useState([]); const PHOTO_STORAGE = 'photos'; @@ -93,7 +102,10 @@ The second parameter, the empty dependency array (`[]`), is what tells React to On mobile (coming up next!), we can directly set the source of an image tag - `` - to each photo file on the `Filesystem`, displaying them automatically. On the web, however, we must read each image from the `Filesystem` into base64 format, using a new `base64` property on the `Photo` object. This is because the `Filesystem` API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Add the following code to complete the `loadSaved()` method. ```ts -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { const [photos, setPhotos] = useState([]); const PHOTO_STORAGE = 'photos'; @@ -126,13 +138,16 @@ export function usePhotoGallery() { `usePhotoGallery.ts` should now look like this: ```ts -import { useState, useEffect } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; +import { useState, useEffect } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { const [photos, setPhotos] = useState([]); const PHOTO_STORAGE = 'photos'; diff --git a/docs/react/your-first-app/5-adding-mobile.mdx b/docs/react/your-first-app/5-adding-mobile.mdx index c0e285d68f..535ff960fb 100644 --- a/docs/react/your-first-app/5-adding-mobile.mdx +++ b/docs/react/your-first-app/5-adding-mobile.mdx @@ -22,13 +22,15 @@ Import the Ionic [Platform API](../platform.mdx) into `usePhotoGallery.ts`, whic Add `isPlatform` to the imports at the top of the file to use the `isPlatform` method. `Capacitor` is also imported to help with file paths on mobile devices. ```ts -import { useState, useEffect } from 'react'; -import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +// CHANGE: Add import +import { Capacitor } from '@capacitor/core'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; // CHANGE: Add import import { isPlatform } from '@ionic/react'; -import { Capacitor } from '@capacitor/core'; +import { useState, useEffect } from 'react'; // ...existing code... ``` @@ -107,15 +109,18 @@ Our Photo Gallery now consists of one codebase that runs on the web, Android, an `usePhotoGallery.ts` should now look like this: ```ts -import { useState, useEffect } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { Capacitor } from '@capacitor/core'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; import { isPlatform } from '@ionic/react'; -import { Capacitor } from '@capacitor/core'; +import { useState, useEffect } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + photos: UserPhoto[]; +} { const [photos, setPhotos] = useState([]); const PHOTO_STORAGE = 'photos'; diff --git a/docs/react/your-first-app/7-live-reload.mdx b/docs/react/your-first-app/7-live-reload.mdx index f93cf02adb..aa6f97e515 100644 --- a/docs/react/your-first-app/7-live-reload.mdx +++ b/docs/react/your-first-app/7-live-reload.mdx @@ -38,15 +38,20 @@ With Live Reload running and the app open on your device, let’s implement phot In `usePhotoGallery.ts`, add the `deletePhoto()` method. The selected photo is removed from the `photos` array first. Then, we delete the actual photo file itself using the Filesystem API. ```ts -import { useState, useEffect } from 'react'; -import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; import type { Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { Capacitor } from '@capacitor/core'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; import { isPlatform } from '@ionic/react'; -import { Capacitor } from '@capacitor/core'; +import { useState, useEffect } from 'react'; -export function usePhotoGallery() { +export function usePhotoGallery(): { + addNewToGallery: () => Promise; + // CHANGE: Add `deletePhoto()` to the return type + deletePhoto: (photo: UserPhoto) => Promise; + photos: UserPhoto[]; +} { // ...existing code... // CHANGE: Add `deletePhoto()` method @@ -68,10 +73,10 @@ export function usePhotoGallery() { }; return { - photos, addNewToGallery, // CHANGE: Add `deletePhoto()` to the return statement deletePhoto, + photos, }; } @@ -84,10 +89,6 @@ export interface UserPhoto { Next, in `Tab2.tsx`, implement the `IonActionSheet` component. We're adding two options: "Delete", which calls `usePhotoGallery.deletePhoto()`, and "Cancel". The cancel button will automatically close the action sheet when assigned the "cancel" role. ```tsx -// CHANGE: Add import -import { useState } from 'react'; -// CHANGE: Update import -import { camera, trash, close } from 'ionicons/icons'; // CHANGE: Update import import { IonContent, @@ -103,6 +104,11 @@ import { IonCol, IonActionSheet, } from '@ionic/react'; +// CHANGE: Update import +import { camera, trash, close } from 'ionicons/icons'; +// CHANGE: Add import +import { useState } from 'react'; + // CHANGE: Add import import type { UserPhoto } from '../hooks/usePhotoGallery'; import { usePhotoGallery } from '../hooks/usePhotoGallery'; diff --git a/docs/vue/quickstart.mdx b/docs/vue/quickstart.mdx index 9be48c4edb..9e21355356 100644 --- a/docs/vue/quickstart.mdx +++ b/docs/vue/quickstart.mdx @@ -97,10 +97,11 @@ Routes are defined in `router/index.ts`: ```ts title="src/router/index.ts" import { createRouter, createWebHistory } from '@ionic/vue-router'; -import { RouteRecordRaw } from 'vue-router'; +import type { RouteRecordRaw } from 'vue-router'; + import HomePage from '../views/HomePage.vue'; -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/home', @@ -240,7 +241,7 @@ import NewPage from '../views/NewPage.vue'; Then, add its route in the `routes` array: ```ts title="src/router/index.ts" -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/home', @@ -375,25 +376,23 @@ Ionic Vue projects are created with TypeScript by default, but you can easily co 1. Remove the TypeScript dependencies: ```shell -npm uninstall --save typescript @types/jest @typescript-eslint/eslint-plugin @typescript-eslint/parser @vue/cli-plugin-typescript @vue/eslint-config-typescript vue-tsc +npm uninstall --save typescript @ionic/eslint-config @vue/eslint-config-typescript vue-tsc ``` -2. Change the extension of all `.ts` files to `.js`. In a blank Ionic Vue app, this will be the `src/router/index.ts`, `src/main.ts`, and files in the `tests` directory. +2. Change the extension of all `.ts` files to `.js`. In a blank Ionic Vue app, this will be the `src/router/index.ts`, `src/main.ts`, `vite.config.ts`, `playwright.config.ts`, and files in the `tests` directory. 3. In `index.html`, change the imported `