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..89c4ce5f26 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 @@ -243,4 +243,29 @@ Next, open `src/app/tabs/tabs.page.html`. Change the label to "Photos" and the ` ``` +Then open `src/app/tabs/tabs.page.ts` and register the `images` icon in place of `ellipse`, so the new icon can display: + +```ts +import { Component, EnvironmentInjector, inject } from '@angular/core'; +import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/angular'; +import { addIcons } from 'ionicons'; +// CHANGE: Update import +import { triangle, images, square } from 'ionicons/icons'; + +@Component({ + selector: 'app-tabs', + templateUrl: 'tabs.page.html', + styleUrls: ['tabs.page.scss'], + imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel], +}) +export class TabsPage { + public environmentInjector = inject(EnvironmentInjector); + + constructor() { + // CHANGE: Register the `images` icon instead of `ellipse` + addIcons({ triangle, images, square }); + } +} +``` + That’s just the start of all the cool things we can do with Ionic. Up next, implement camera taking functionality on the web, then build it for iOS and Android. diff --git a/docs/angular/your-first-app/2-taking-photos.mdx b/docs/angular/your-first-app/2-taking-photos.mdx index 898e2d6fc9..f9bea16149 100644 --- a/docs/angular/your-first-app/2-taking-photos.mdx +++ b/docs/angular/your-first-app/2-taking-photos.mdx @@ -45,12 +45,12 @@ 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, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); } } @@ -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 + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); // CHANGE: Add the new photo to the front of the photos signal @@ -207,12 +208,12 @@ 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, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); this.photos.update((photos) => [ diff --git a/docs/angular/your-first-app/3-saving-photos.mdx b/docs/angular/your-first-app/3-saving-photos.mdx index 5529c6dbd2..73eac223d1 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,12 +57,12 @@ 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, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); // CHANGE: Add `savedImageFile` @@ -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,12 +165,12 @@ 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, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); // Save the picture and add it to photo collection diff --git a/docs/angular/your-first-app/4-loading-photos.mdx b/docs/angular/your-first-app/4-loading-photos.mdx index 5a5b5fc626..6b92091810 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,12 +47,12 @@ 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, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); const savedImageFile = await this.savePicture(capturedPhoto); @@ -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,12 +137,12 @@ 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, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); // Save the picture and add it to photo collection @@ -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..864c1062bd 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,12 +195,12 @@ 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, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); const savedImageFile = await this.savePicture(capturedPhoto); @@ -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/introduction.mdx b/docs/react/testing/introduction.mdx index 25698b2956..94ba66f2ae 100644 --- a/docs/react/testing/introduction.mdx +++ b/docs/react/testing/introduction.mdx @@ -10,7 +10,20 @@ This document provides an overview of how to test an application built with `@io ## Introduction {/* #introduction */} -Testing is an important part of the development process, and it helps to ensure that an application is working as intended. In `@ionic/react`, testing is done using a combination of tools and libraries, including Jest or Vitest, React Testing Library, Playwright or Cypress. +Testing is an important part of the development process, and it helps to ensure that an application is working as intended. Applications generated with the Ionic CLI are set up for unit testing with [Vitest](https://vitest.dev) and [React Testing Library](https://testing-library.com), and for end-to-end testing with [Playwright](https://playwright.dev). Unit tests live next to the code they test in `src` and end-to-end tests live in `e2e`. + +The generated `package.json` includes scripts for running them: + +| Script | Description | +| ------------------------- | ------------------------------------------------------------ | +| `npm test` | Runs the unit tests once, then the end-to-end tests. | +| `npm run test:unit` | Runs the unit tests once. | +| `npm run test:unit:watch` | Runs the unit tests and re-runs them when files change. | +| `npm run test:e2e` | Runs the end-to-end tests, starting the dev server for them. | + +:::note +Playwright downloads its browsers separately from the npm package. Run `npx playwright install` once before running the end-to-end tests for the first time. +::: ## Types of Tests {/* #types-of-tests */} @@ -18,4 +31,4 @@ There are two types of tests that can be written: **Unit Tests**: Unit tests are used to test individual functions and components in isolation. [Jest](https://jestjs.io), [Vitest](https://vitest.dev/) and [React Testing Library](https://testing-library.com) are commonly used for unit testing. -**Integration Tests**: Integration tests are used to test how different components work together. [Cypress](https://www.cypress.io) or [Playwright](https://playwright.dev) are commonly used for integration testing. +**End-to-end Tests**: End-to-end tests are used to test how different components work together in the running application. [Playwright](https://playwright.dev) and [Cypress](https://www.cypress.io) are commonly used for end-to-end testing. diff --git a/docs/react/testing/unit-testing/best-practices.mdx b/docs/react/testing/unit-testing/best-practices.mdx index 1a705eb4db..2e85bf7932 100644 --- a/docs/react/testing/unit-testing/best-practices.mdx +++ b/docs/react/testing/unit-testing/best-practices.mdx @@ -10,7 +10,8 @@ In your test template when rendering with React Testing Library, you must wrap y ```tsx title="Example.test.tsx" import { IonApp } from '@ionic/react'; -import { render } from "@testing-library/react"; +import { render } from '@testing-library/react'; +import { test } from 'vitest'; import Example from './Example'; @@ -30,8 +31,9 @@ React Testing Library recommends using the `user-event` library for simulating u ```tsx title="Example.test.tsx" import { IonApp } from '@ionic/react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { test } from 'vitest'; import Example from './Example'; @@ -55,9 +57,10 @@ For more information on `user-event`, refer to the [user-event documentation](ht When you need to wait for an Ionic component to render before asserting against its DOM, use the `componentOnReady` helper exported from `@ionic/core`. Do not call `el.componentOnReady()` directly. `@ionic/react` uses Stencil's custom elements build, where that method does not exist on the element. The helper waits one animation frame instead, giving the component's inner contents a chance to render. ```tsx +import { componentOnReady } from '@ionic/core'; import { IonApp } from '@ionic/react'; import { render } from '@testing-library/react'; -import { componentOnReady } from '@ionic/core'; +import { expect, test } from 'vitest'; import Example from './Example'; diff --git a/docs/react/testing/unit-testing/examples.mdx b/docs/react/testing/unit-testing/examples.mdx index 1b5a84ac10..9186aaf917 100644 --- a/docs/react/testing/unit-testing/examples.mdx +++ b/docs/react/testing/unit-testing/examples.mdx @@ -30,6 +30,7 @@ export default function Example() { ```tsx title="src/Example.test.tsx" import { IonApp } from '@ionic/react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { expect, test } from 'vitest'; import Example from './Example'; @@ -92,6 +93,7 @@ export default Example; ```tsx title="src/Example.test.tsx" import { IonApp } from '@ionic/react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { expect, test } from 'vitest'; import Example from './Example'; diff --git a/docs/react/testing/unit-testing/setup.mdx b/docs/react/testing/unit-testing/setup.mdx index aae81f5703..2ca4acd782 100644 --- a/docs/react/testing/unit-testing/setup.mdx +++ b/docs/react/testing/unit-testing/setup.mdx @@ -6,35 +6,79 @@ 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, Vitest, 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 Vitest {/* #install-vitest */} + +[Vitest](https://vitest.dev) runs the tests. It uses [jsdom](https://github.com/jsdom/jsdom) to provide a browser-like environment for the components. + +```bash +npm install --save-dev vitest jsdom +``` + +Then configure Vitest in `vite.config.ts` to use jsdom and load `src/setupTests.ts` before the tests run: + +```ts title="vite.config.ts" +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + setupFiles: './src/setupTests.ts', + include: ['src/**/*.test.{ts,tsx}'], + }, +}); +``` ### 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(); ++ +// Testing Library only cleans up automatically when Vitest globals are enabled. +afterEach(() => { + cleanup(); +}); -// Mock matchmedia -window.matchMedia = window.matchMedia || function () { - return { - matches: false, - addListener: function () { }, - removeListener: function () { } +// 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..91d8e8d890 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,12 +253,16 @@ 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'; /* ...existing Ionic styles... */ +setupIonicReact(); + const App: React.FC = () => ( diff --git a/docs/react/your-first-app/2-taking-photos.mdx b/docs/react/your-first-app/2-taking-photos.mdx index b65238b780..1bc8f47ae9 100644 --- a/docs/react/your-first-app/2-taking-photos.mdx +++ b/docs/react/your-first-app/2-taking-photos.mdx @@ -24,13 +24,15 @@ 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({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); }; @@ -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,20 +132,24 @@ 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 const addNewToGallery = async () => { // Take a photo const capturedPhoto = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); // CHANGE: Create the `fileName` with current timestamp - const fileName = Date.now() + '.jpeg'; + const fileName = new Date().getTime() + '.jpeg'; // CHANGE: Create `savedImageFile` matching `UserPhoto` interface const savedImageFile = [ { @@ -163,21 +174,24 @@ 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 () => { // Take a photo const capturedPhoto = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); - const fileName = Date.now() + '.jpeg'; + const fileName = new Date().getTime() + '.jpeg'; const savedImageFile = [ { filepath: fileName, @@ -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..029ad1107f 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,22 +52,25 @@ 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 () => { // Take a photo const capturedPhoto = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); - const fileName = Date.now() + '.jpeg'; + const fileName = new Date().getTime() + '.jpeg'; // CHANGE: Add `savedImageFile` // Save the picture and add it to photo collection const savedImageFile = await savePicture(capturedPhoto, fileName); @@ -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,23 +169,26 @@ 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 () => { // Take a photo const capturedPhoto = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); - const fileName = Date.now() + '.jpeg'; + const fileName = new Date().getTime() + '.jpeg'; // Save the picture and add it to photo collection const savedImageFile = await savePicture(capturedPhoto, fileName); diff --git a/docs/react/your-first-app/4-loading-photos.mdx b/docs/react/your-first-app/4-loading-photos.mdx index 6bd1465a38..6c4712b303 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'; @@ -159,12 +174,12 @@ export function usePhotoGallery() { const addNewToGallery = async () => { // Take a photo const capturedPhoto = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); - const fileName = Date.now() + '.jpeg'; + const fileName = new Date().getTime() + '.jpeg'; // Save the picture and add it to photo collection const savedImageFile = await savePicture(capturedPhoto, fileName); diff --git a/docs/react/your-first-app/5-adding-mobile.mdx b/docs/react/your-first-app/5-adding-mobile.mdx index c0e285d68f..0bfb1ab6df 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... ``` @@ -49,7 +51,7 @@ const savePicture = async (photo: Photo, fileName: string): Promise = const file = await Filesystem.readFile({ path: photo.path!, }); - base64Data = typeof file.data === 'string' ? file.data : await file.data.text(); + base64Data = file.data; } else { base64Data = await base64FromPath(photo.webPath!); } @@ -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'; @@ -145,12 +150,12 @@ export function usePhotoGallery() { const addNewToGallery = async () => { // Take a photo const capturedPhoto = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, + resultType: CameraResultType.Uri, // file-based data; provides best performance + source: CameraSource.Camera, // automatically take a new photo with the camera + quality: 100, // highest quality (0 to 100) }); - const fileName = Date.now() + '.jpeg'; + const fileName = new Date().getTime() + '.jpeg'; // Save the picture and add it to photo collection const savedImageFile = await savePicture(capturedPhoto, fileName); @@ -167,7 +172,7 @@ export function usePhotoGallery() { const file = await Filesystem.readFile({ path: photo.path!, }); - base64Data = typeof file.data === 'string' ? file.data : await file.data.text(); + base64Data = file.data; } else { base64Data = await base64FromPath(photo.webPath!); } 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/navigation.mdx b/docs/vue/navigation.mdx index 84e8981f75..957bf22afa 100644 --- a/docs/vue/navigation.mdx +++ b/docs/vue/navigation.mdx @@ -31,10 +31,11 @@ Here is a sample routing configuration that defines a single route to the "/home ```tsx import { createRouter, createWebHistory } from '@ionic/vue-router'; -import { RouteRecordRaw } from 'vue-router'; +import type { RouteRecordRaw } from 'vue-router'; + import HomePage from '@/views/Home.vue'; -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/', name: 'Home', @@ -57,7 +58,7 @@ On the app's initial load, the app will render the `HomePage` component as that What if we wanted to land a different path on our initial load? For this, we can use router redirects. Redirects work the same way that a typical route object does, but just includes some different keys: ```tsx -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/home', @@ -77,7 +78,7 @@ In our redirect, we look for the index path of our app. Then if we load that, we This is all great, but how does one actually navigate to a route? For this, we can use the `router-link` property. Let's create a new routing setup: ```tsx -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/home', @@ -187,7 +188,7 @@ A key characteristic of `router.go()` is that it expects your application histor The current way our routes are setup makes it so they are included in the same initial chunk when loading the app, which is not always ideal. Instead, we can set up our routes so that components are loaded as they are needed: ```tsx -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/home', @@ -280,7 +281,7 @@ A common point of confusion when setting up routing is deciding between shared U Shared URLs is a route configuration where routes have pieces of the URL in common. The following is an example of a shared URL configuration: ```tsx -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/dashboard', component: DashboardMainPage, @@ -299,7 +300,7 @@ The above routes are considered "shared" because they reuse the `dashboard` piec Nested Routes is a route configuration where routes are listed as children of other routes. The following is an example of a nested route configuration: ```tsx -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/dashboard/:id', component: DashboardRouterOutlet, @@ -332,7 +333,7 @@ There are very few use cases in which nested routes make sense in mobile applica When working with tabs, Ionic Vue needs a way to know which view belongs to which tab. The `IonTabs` component comes in handy here, but let's examine the routing setup for this: ```tsx -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/tabs/tab1', @@ -412,7 +413,7 @@ Since Ionic is focused on helping developers build mobile apps, the tabs in Ioni When adding additional routes to tabs you should write them as sibling routes with the parent tab as the path prefix. The example below defines the `/tabs/tab1/view` route as a sibling of the `/tabs/tab1` route. Since this new route has the `tab1` prefix, it will be rendered inside of the `Tabs` component, and Tab 1 will still be selected in the `ion-tab-bar`. ```tsx -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/tabs/tab1', @@ -532,7 +533,7 @@ For example usages, please refer to our [Utility Functions](utility-functions#us Let's expand upon our original routing example to show how we can use URL parameters. We recommend [passing URL parameters as props](https://router.vuejs.org/guide/essentials/passing-props.html) so that the component does not need a direct reference to the router, which makes it easier to reuse and test in isolation. ```tsx -const routes: Array = [ +const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/home', diff --git a/docs/vue/quickstart.mdx b/docs/vue/quickstart.mdx index 9be48c4edb..6f116429b9 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,24 +376,59 @@ 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 @types/node @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`, except `src/vite-env.d.ts`, which is deleted in a later step. 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 `