diff --git a/docs/components.md b/docs/components.md
index 4d1b2cf28a2..e3add853aa8 100644
--- a/docs/components.md
+++ b/docs/components.md
@@ -32,6 +32,10 @@ Ionic apps are made of high-level building blocks called Components, which allow
Action Sheets display a set of options with the ability to confirm or cancel an action.
+
+ Action Sheets display a set of options with the ability to confirm or cancel an action.
+
+
Alerts are a great way to offer the user the ability to choose a specific action or list of actions.
diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md
index b1fe24b6ec5..5a147cad92c 100644
--- a/docs/react/your-first-app.md
+++ b/docs/react/your-first-app.md
@@ -102,10 +102,22 @@ After installation, open up the project in your code editor of choice.
Next, import `@ionic/pwa-elements` by editing `src/main.tsx`.
```tsx
+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';
-// Call the element loader before the render call
+// CHANGE: Call the element loader before the render call
defineCustomElements(window);
+
+const container = document.getElementById('root');
+const root = createRoot(container!);
+root.render(
+
+
+
+);
```
That’s it! Now for the fun part - let’s see the app in action.
@@ -147,10 +159,12 @@ Open `/src/pages/Tab2.tsx`. We see:
Photo Gallery
```
-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](https://ionicframework.com/docs/api/fab) (FAB). First, update the imports at the top of the page to include the Camera icon as well as some of the Ionic components we'll use shortly:
+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](https://ionicframework.com/docs/api/fab) (FAB) to the bottom of the page and set the camera image as the icon.
```tsx
+// CHANGE: Add the following import.
import { camera, trash, close } from 'ionicons/icons';
+// CHANGE: Add the following import.
import {
IonContent,
IonHeader,
@@ -166,22 +180,33 @@ import {
IonImg,
IonActionSheet,
} from '@ionic/react';
+import ExploreContainer from '../components/ExploreContainer';
+import './Tab2.css';
+
+const Tab2: React.FC = () => {
+ return (
+
+
+
+ Tab 2
+
+
+
+
+
+ takePhoto()}>
+
+
+
+
+
+
+ );
+};
+
+export default Tab2;
```
-Then, add the FAB to the bottom of the page. Use the camera image as the icon, and call the `takePhoto()` function when this button is clicked (to be implemented soon):
-
-```tsx
-
-
- takePhoto()}>
-
-
-
-
-```
-
-We’ll be creating the `takePhoto` method and the logic to use the Camera and other native features in a moment.
-
Next, open `src/App.tsx`, remove the `ellipse` icon from the import and import the `images` icon instead:
```tsx
@@ -191,10 +216,47 @@ import { images, square, triangle } from 'ionicons/icons';
Within the tab bar (``), change the label to “Photos” and the `ellipse` icon to `images` for the middle tab button:
```tsx
-
-
- Photos
-
+// Keep other imports
+// CHANGE: Add the following import.
+import { images, square, triangle } from 'ionicons/icons';
+
+const App: React.FC = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tab 1
+
+
+
+
+ Photos
+
+
+
+ Tab 3
+
+
+
+
+
+);
```
:::note
diff --git a/docs/react/your-first-app/2-taking-photos.md b/docs/react/your-first-app/2-taking-photos.md
index 64b402d6aec..e0b3eafd32e 100644
--- a/docs/react/your-first-app/2-taking-photos.md
+++ b/docs/react/your-first-app/2-taking-photos.md
@@ -24,20 +24,34 @@ Create a new file at `src/hooks/usePhotoGallery.ts` and open it up.
A custom hook is just a function that uses other React hooks. And that's what we will be doing! We will start by importing the various hooks and utilities we will be using from React core, the Ionic React Hooks project, and Capacitor:
```tsx
+// CHANGE: Add the following imports
import { useState, useEffect } from 'react';
import { isPlatform } from '@ionic/react';
+// CHANGE: Add the following imports
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';
import { Capacitor } from '@capacitor/core';
+
+export function usePhotoGallery() {}
```
Next, create a function named usePhotoGallery:
```tsx
+import { useState, useEffect } from 'react';
+import { isPlatform } from '@ionic/react';
+
+import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
+import { Filesystem, Directory } from '@capacitor/filesystem';
+import { Preferences } from '@capacitor/preferences';
+import { Capacitor } from '@capacitor/core';
+
export function usePhotoGallery() {
+ // CHANGE: Add the usePhotoGallery function.
const takePhoto = async () => {
+ // Take a photo
const photo = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
@@ -58,16 +72,34 @@ Notice the magic here: there's no platform-specific code (web, iOS, or Android)!
The last step we need to take is to use the new hook from the Tab2 page. Go back to Tab2.tsx and import the hook:
```tsx
-import { usePhotoGallery } from '../hooks/usePhotoGallery';
-```
+// Keep the other imports
-And right before the return statement in the functional component, get access to the `takePhoto` method by using the hook:
+// CHANGE: Import the usePhotoGallery hook
+import { usePhotoGallery } from '../hooks/usePhotoGallery';
-```tsx
const Tab2: React.FC = () => {
+ // CHANGE: Get access to `takePhoto` method by using the hook
const { takePhoto } = usePhotoGallery();
- // snip - rest of code
+ return (
+
+
+
+ Tab 2
+
+
+
+
+ takePhoto()}>
+
+
+
+
+
+ );
+};
+
+export default Tab2;
```
Save the file, and if you’re not already, restart the development server in your browser by running `ionic serve`. On the Photo Gallery tab, click the Camera button. If your computer has a webcam of any sort, a modal window appears. Take a selfie!
@@ -83,6 +115,11 @@ After taking a photo, it disappears. We still need to display it within our app
First we will create a new type to define our Photo, which will hold specific metadata. Add the following UserPhoto interface to the `usePhotoGallery.ts` file, somewhere outside of the main function:
```tsx
+export functino usePhotoGallery {
+ // Same old code from before.
+}
+
+// CHANGE: Add the interface.
export interface UserPhoto {
filepath: string;
webviewPath?: string;
@@ -92,53 +129,130 @@ export interface UserPhoto {
Back at the top of the function (right after the call to `usePhotoGallery`, we will define a state variable to store the array of each photo captured with the Camera.
```tsx
-const [photos, setPhotos] = useState([]);
+export function usePhotoGallery {
+ // CHANGE: Add the photos array.
+ const [photos, setPhotos] = useState([]);
+
+ // Same old code from before.
+}
```
When the camera is done taking a picture, the resulting Photo returned from Capacitor will be stored in the `photo` variable. We want to create a new photo object and add it to the photos state array. We make sure we don't accidentally mutate the current photos array by making a new array, and then call `setPhotos` to store the array into state. Update the `takePhoto` method and add this code after the getPhoto call:
```tsx
-const fileName = Date.now() + '.jpeg';
-const newPhotos = [
- {
- filepath: fileName,
- webviewPath: photo.webPath,
- },
- ...photos,
-];
-setPhotos(newPhotos);
-```
+// Same old code from before.
-Next, let's expose the photos array from our hook. Update the return statement to include the photos:
+export function usePhotoGallery() {
+ const [photos, setPhotos] = useState([]);
+ // CHANGE: Create new fileName variable with date and .jpeg
+ const fileName = Date.now() + '.jpeg';
-```tsx
-return {
- photos,
- takePhoto,
-};
+ const takePhoto = async () => {
+ // Same old code from before.
+
+ // CHANGE: Add in newPhotos after getPhoto call
+ const newPhotos = [
+ {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ },
+ ...photos,
+ ];
+ setPhotos(newPhotos);
+ };
+
+ // CHANGE: Update return statement to include photos.
+ return {
+ photos,
+ takePhoto,
+ };
+}
+
+// Same old code from before.
```
-And back in the Tab2 component, get access to the photos:
+`usePhotoGallery.ts` should now look like this:
```tsx
-const { photos, takePhoto } = usePhotoGallery();
+import { useState, useEffect } from 'react';
+import { isPlatform } from '@ionic/react';
+import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
+import { Filesystem, Directory } from '@capacitor/filesystem';
+import { Preferences } from '@capacitor/preferences';
+import { Capacitor } from '@capacitor/core';
+
+export function usePhotoGallery() {
+ const [photos, setPhotos] = useState([]);
+ const fileName = Date.now() + '.jpeg';
+
+ const takePhoto = async () => {
+ const photo = await Camera.getPhoto({
+ resultType: CameraResultType.Uri,
+ source: CameraSource.Camera,
+ quality: 100,
+ });
+
+ const newPhotos = [
+ {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ },
+ ...photos,
+ ];
+ setPhotos(newPhotos);
+ };
+
+ return {
+ photos,
+ takePhoto,
+ };
+}
+
+export interface UserPhoto {
+ filepath: string;
+ webviewPath?: string;
+}
```
-With the photo(s) stored into the main array we can display the images on the screen. Add a [Grid component](https://ionicframework.com/docs/api/grid) so that each photo will display nicely as photos are added to the gallery, and loop through each photo in the Photos array, adding an Image component (``) for each. Point the `src` (source) to the photo’s path:
+Next, move over to `Tab2.tsx` so we can display the image on the screen. With the photo(s) stored into the main array we can display the images on the screen. Add a [Grid component](https://ionicframework.com/docs/api/grid) so that each photo will display nicely as photos are added to the gallery, and loop through each photo in the Photos array, adding an Image component (``) for each. Point the `src` (source) to the photo’s path:
```tsx
-
-
-
- {photos.map((photo, index) => (
-
-
-
- ))}
-
-
-
-
+// Same old code from before.
+
+// CHANGE: Import usePhotoGallery Hook
+import { usePhotoGallery } from '../hooks/usePhotoGallery';
+
+const Tab2: React.FC = () => {
+ // CHANGE: Get access to photos from usePhotoGallery
+ const { photos, takePhoto } = usePhotoGallery();
+
+ return (
+
+
+
+ Tab 2
+
+
+
+
+
+
+ {photos.map((photo, index) => (
+
+
+
+ ))}
+
+
+
+ takePhoto()}>
+
+
+
+
+
+ );
+};
```
Save all files. Within the web browser, click the Camera button and take another photo. This time, the photo is displayed in the Photo Gallery!
diff --git a/docs/react/your-first-app/3-saving-photos.md b/docs/react/your-first-app/3-saving-photos.md
index eae9a61df7b..aa54812590a 100644
--- a/docs/react/your-first-app/3-saving-photos.md
+++ b/docs/react/your-first-app/3-saving-photos.md
@@ -17,7 +17,17 @@ We will use the `writeFile` method initially, but we will use the others coming
Next, create a couple of new functions in `usePhotoGallery`:
```tsx
+import { useState, useEffect } from 'react';
+import { isPlatform } from '@ionic/react';
+import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
+import { Filesystem, Directory } from '@capacitor/filesystem';
+import { Preferences } from '@capacitor/preferences';
+import { Capacitor } from '@capacitor/core';
+
export function usePhotoGallery() {
+ // Same old code from before.
+
+ // CHANGE: Add in new function to save pictures
const savePicture = async (photo: Photo, fileName: string): Promise => {
const base64Data = await base64FromPath(photo.webPath!);
const savedFile = await Filesystem.writeFile({
@@ -33,8 +43,11 @@ export function usePhotoGallery() {
webviewPath: photo.webPath,
};
};
+
+ // Same old code from before.
}
+// CHANGE: Add a function that allows the photo to be downloaded from the supplied path
export async function base64FromPath(path: string): Promise {
const response = await fetch(path);
const blob = await response.blob();
@@ -51,6 +64,8 @@ export async function base64FromPath(path: string): Promise {
reader.readAsDataURL(blob);
});
}
+
+// Same old code from before.
```
:::note
@@ -64,18 +79,109 @@ Next we use the Capacitor [Filesystem API](https://capacitorjs.com/docs/apis/fil
Last, call `savePicture` and pass in the photo object and filename directly underneath the call to `setPhotos` in the `takePhoto` method. Here is the full method:
```tsx
-const takePhoto = async () => {
- const photo = await Camera.getPhoto({
- resultType: CameraResultType.Uri,
- source: CameraSource.Camera,
- quality: 100,
- });
+// Same old code from before.
+
+export function usePhotoGallery() {
+ // Same old code from before.
+
+ // CHANGE: Update the takePhoto function to utilize capacitor filesystem
+ const takePhoto = async () => {
+ const photo = await Camera.getPhoto({
+ resultType: CameraResultType.Uri,
+ source: CameraSource.Camera,
+ quality: 100,
+ });
+
+ const newPhotos = [
+ {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ },
+ ...photos,
+ ];
+ setPhotos(newPhotos);
+ };
+
+ // Same old code from before
+}
+
+// Same old code from before.
+```
+
+`usePhotoGallery.ts` should now look like this:
+```tsx
+import { useState, useEffect } from 'react';
+import { isPlatform } from '@ionic/react';
+import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
+import { Filesystem, Directory } from '@capacitor/filesystem';
+import { Preferences } from '@capacitor/preferences';
+import { Capacitor } from '@capacitor/core';
+
+export function usePhotoGallery() {
+ const [photos, setPhotos] = useState([]);
const fileName = Date.now() + '.jpeg';
- const savedFileImage = await savePicture(photo, fileName);
- const newPhotos = [savedFileImage, ...photos];
- setPhotos(newPhotos);
-};
+
+ const savePicture = async (photo: Photo, fileName: string): Promise => {
+ const base64Data = await base64FromPath(photo.webPath!);
+ const savedFile = await Filesystem.writeFile({
+ path: fileName,
+ data: base64Data,
+ directory: Directory.Data,
+ });
+
+ // Use webPath to display the new image instead of base64 since it's
+ // already loaded into memory
+ return {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ };
+ };
+
+ const takePhoto = async () => {
+ const photo = await Camera.getPhoto({
+ resultType: CameraResultType.Uri,
+ source: CameraSource.Camera,
+ quality: 100,
+ });
+
+ const newPhotos = [
+ {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ },
+ ...photos,
+ ];
+ setPhotos(newPhotos);
+ };
+
+ return {
+ photos,
+ takePhoto,
+ };
+}
+
+export async function base64FromPath(path: string): Promise {
+ const response = await fetch(path);
+ const blob = await response.blob();
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onerror = reject;
+ reader.onload = () => {
+ if (typeof reader.result === 'string') {
+ resolve(reader.result);
+ } else {
+ reject('method did not return a string');
+ }
+ };
+ reader.readAsDataURL(blob);
+ });
+}
+
+export interface UserPhoto {
+ filepath: string;
+ webviewPath?: string;
+}
```
There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem.
diff --git a/docs/react/your-first-app/4-loading-photos.md b/docs/react/your-first-app/4-loading-photos.md
index 75c1cb8e901..4ef6ef6284c 100644
--- a/docs/react/your-first-app/4-loading-photos.md
+++ b/docs/react/your-first-app/4-loading-photos.md
@@ -20,7 +20,10 @@ Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](http
Begin by defining a constant variable that will act as the key for the store before the `usePhotoGallery` function definition in `src/hooks/usePhotoGallery.ts`:
```tsx
+// CHANGE: Create a constant variable that will act as a key to store
const PHOTO_STORAGE = 'photos';
+
+// Same old code from before
export function usePhotoGallery() {}
```
@@ -29,29 +32,70 @@ Then, use the `Storage` class to get access to the get and set methods for readi
At the end of the `takePhoto` function, 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.
```tsx
-Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
+// Same old code from before.
+export function usePhotoGallery() {
+ // Same old code from before.
+
+ const takePhoto = async () => {
+ const photo = await Camera.getPhoto({
+ resultType: CameraResultType.Uri,
+ source: CameraSource.Camera,
+ quality: 100,
+ });
+
+ const newPhotos = [
+ {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ },
+ ...photos,
+ ];
+ setPhotos(newPhotos);
+ // CHANGE: Add a call to save the photos array
+ Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
+ };
+ // Same old code from before
+ return {
+ photos,
+ takePhoto,
+ };
+}
+
+// Same old code from before.
```
With the photo array data saved, we will create a method that will retrieve the data when the hook loads. We will do so by using React's `useEffect` hook. Insert this above the `takePhoto` declaration. Here is the code, and we will break it down:
```tsx
-useEffect(() => {
- const loadSaved = async () => {
- const { value } = await Preferences.get({ key: PHOTO_STORAGE });
- const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
-
- for (let photo of photosInPreferences) {
- const file = 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}`;
- }
- setPhotos(photosInPreferences);
+// Same old code from before.
+export function usePhotoGallery() {
+ // Same old code from before.
+
+ // CHANGE: Add useEffect hook
+ useEffect(() => {
+ const loadSaved = async () => {
+ const { value } = await Preferences.get({ key: PHOTO_STORAGE });
+ const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
+
+ for (let photo of photosInPreferences) {
+ const file = 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}`;
+ }
+ setPhotos(photosInPreferences);
+ };
+ loadSaved();
+ }, []);
+
+ const takePhotos = async () => {
+ // Same old code from before.
};
- loadSaved();
-}, []);
+}
+
+// Same old code from before.
```
This seems a bit scary at first, so let's walk through it, first by looking at the second parameter we pass into the hook: the dependency array `[]`.
@@ -62,4 +106,100 @@ The first parameter to `useEffect` is the function that will be called by the ef
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, because the Filesystem API stores them in base64 within [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood.
+`usePhotoGallery.ts` should now look like this:
+
+```tsx
+import { useState, useEffect } from 'react';
+import { isPlatform } from '@ionic/react';
+import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
+import { Filesystem, Directory } from '@capacitor/filesystem';
+import { Preferences } from '@capacitor/preferences';
+import { Capacitor } from '@capacitor/core';
+
+const PHOTO_STORAGE = 'photos';
+
+export function usePhotoGallery() {
+ const [photos, setPhotos] = useState([]);
+ const fileName = Date.now() + '.jpeg';
+ const savePicture = async (photo: Photo, fileName: string): Promise => {
+ const base64Data = await base64FromPath(photo.webPath!);
+ const savedFile = await Filesystem.writeFile({
+ path: fileName,
+ data: base64Data,
+ directory: Directory.Data,
+ });
+
+ // Use webPath to display the new image instead of base64 since it's
+ // already loaded into memory
+ return {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ };
+ };
+
+ useEffect(() => {
+ const loadSaved = async () => {
+ const { value } = await Preferences.get({ key: PHOTO_STORAGE });
+ const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
+
+ for (let photo of photosInPreferences) {
+ const file = 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}`;
+ }
+ setPhotos(photosInPreferences);
+ };
+ loadSaved();
+ }, []);
+
+ const takePhoto = async () => {
+ const photo = await Camera.getPhoto({
+ resultType: CameraResultType.Uri,
+ source: CameraSource.Camera,
+ quality: 100,
+ });
+
+ const newPhotos = [
+ {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ },
+ ...photos,
+ ];
+ setPhotos(newPhotos);
+ Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
+ };
+
+ return {
+ photos,
+ takePhoto,
+ };
+}
+
+export async function base64FromPath(path: string): Promise {
+ const response = await fetch(path);
+ const blob = await response.blob();
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onerror = reject;
+ reader.onload = () => {
+ if (typeof reader.result === 'string') {
+ resolve(reader.result);
+ } else {
+ reject('method did not return a string');
+ }
+ };
+ reader.readAsDataURL(blob);
+ });
+}
+
+export interface UserPhoto {
+ filepath: string;
+ webviewPath?: string;
+}
+```
+
That’s it! We’ve built a complete Photo Gallery feature in our Ionic app that works on the web. Next up, we’ll transform it into a mobile app for iOS and Android!
diff --git a/docs/react/your-first-app/5-adding-mobile.md b/docs/react/your-first-app/5-adding-mobile.md
index fa1799da9d1..07a0e120329 100644
--- a/docs/react/your-first-app/5-adding-mobile.md
+++ b/docs/react/your-first-app/5-adding-mobile.md
@@ -13,61 +13,199 @@ Let’s start with making some small code changes - then our app will “just wo
First, we’ll update the photo saving functionality to support mobile. In the `savePicture` function, check which platform the app is running on. If it’s “hybrid” (Capacitor or Cordova, the two native runtimes), then read the photo file into base64 format using the `readFile` method. Also, return the complete file path to the photo using the Filesystem API. When setting the `webviewPath`, use the special `Capacitor.convertFileSrc` method ([details here](https://ionicframework.com/docs/core-concepts/webview#file-protocol)). Otherwise, use the same logic as before when running the app on the web.
```tsx
-const savePicture = async (photo: Photo, fileName: string): Promise => {
- let base64Data: string | Blob;
- // "hybrid" will detect Cordova or Capacitor;
- if (isPlatform('hybrid')) {
- const file = await Filesystem.readFile({
- path: photo.path!,
+// Same old code from before.
+export function usePhotoGallery() {
+ // Same old code from before.
+
+ // CHANGE: Update savePicture function
+ const savePicture = async (photo: Photo, fileName: string): Promise => {
+ let base64Data: string | Blob;
+ // "hybrid" will detect Cordova or Capacitor;
+ if (isPlatform('hybrid')) {
+ const file = await Filesystem.readFile({
+ path: photo.path!,
+ });
+ base64Data = file.data;
+ } else {
+ base64Data = await base64FromPath(photo.webPath!);
+ }
+ const savedFile = await Filesystem.writeFile({
+ path: fileName,
+ data: base64Data,
+ directory: Directory.Data,
});
- base64Data = file.data;
- } else {
- base64Data = await base64FromPath(photo.webPath!);
- }
- const savedFile = await Filesystem.writeFile({
- path: fileName,
- data: base64Data,
- directory: Directory.Data,
- });
- if (isPlatform('hybrid')) {
- // Display the new image by rewriting the 'file://' path to HTTP
- // Details: https://ionicframework.com/docs/building/webview#file-protocol
- return {
- filepath: savedFile.uri,
- webviewPath: Capacitor.convertFileSrc(savedFile.uri),
- };
- } else {
- // Use webPath to display the new image instead of base64 since it's
- // already loaded into memory
- return {
- filepath: fileName,
- webviewPath: photo.webPath,
- };
- }
-};
+ if (isPlatform('hybrid')) {
+ // Display the new image by rewriting the 'file://' path to HTTP
+ // Details: https://ionicframework.com/docs/building/webview#file-protocol
+ return {
+ filepath: savedFile.uri,
+ webviewPath: Capacitor.convertFileSrc(savedFile.uri),
+ };
+ } else {
+ // Use webPath to display the new image instead of base64 since it's
+ // already loaded into memory
+ return {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ };
+ }
+ };
+
+ // Same old code from before.
+}
+
+// Same old code from before.
```
Next, add a new bit of logic in the `loadSaved` function. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved` function inside of `useEffect` to:
```tsx
-const loadSaved = async () => {
- const { value } = await Preferences.get({ key: PHOTO_STORAGE });
+// Same old code from before.
+export function usePhotoGallery() {
+ // Same old code from before.
+
+ useEffect(() => {
+ // CHANGE: Update loadSaved function within useEffect
+ const loadSaved = async () => {
+ const { value } = await Preferences.get({ key: PHOTO_STORAGE });
+
+ const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
+ // If running on the web...
+ if (!isPlatform('hybrid')) {
+ for (let photo of photosInPreferences) {
+ const file = 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}`;
+ }
+ }
+ setPhotos(photosInPreferences);
+ };
+ }, []);
+
+ // Same old code from before.
+}
+
+// Same old code from before.
+```
+
+Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. Next up, the part you’ve been waiting for - deploying the app to a device.
+
+`usePhotoGallery.ts` should now look like this:
+
+```tsx
+import { useState, useEffect } from 'react';
+import { isPlatform } from '@ionic/react';
+import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
+import { Filesystem, Directory } from '@capacitor/filesystem';
+import { Preferences } from '@capacitor/preferences';
+import { Capacitor } from '@capacitor/core';
+
+const PHOTO_STORAGE = 'photos';
- const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
- // If running on the web...
- if (!isPlatform('hybrid')) {
- for (let photo of photosInPreferences) {
+export function usePhotoGallery() {
+ const [photos, setPhotos] = useState([]);
+ const fileName = Date.now() + '.jpeg';
+ const savePicture = async (photo: Photo, fileName: string): Promise => {
+ let base64Data: string | Blob;
+ // "hybrid" will detect Cordova or Capacitor;
+ if (isPlatform('hybrid')) {
const file = await Filesystem.readFile({
- path: photo.filepath,
- directory: Directory.Data,
+ path: photo.path!,
});
- // Web platform only: Load the photo as base64 data
- photo.webviewPath = `data:image/jpeg;base64,${file.data}`;
+ base64Data = file.data;
+ } else {
+ base64Data = await base64FromPath(photo.webPath!);
}
- }
- setPhotos(photosInPreferences);
-};
-```
+ const savedFile = await Filesystem.writeFile({
+ path: fileName,
+ data: base64Data,
+ directory: Directory.Data,
+ });
-Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. Next up, the part you’ve been waiting for - deploying the app to a device.
+ if (isPlatform('hybrid')) {
+ // Display the new image by rewriting the 'file://' path to HTTP
+ // Details: https://ionicframework.com/docs/building/webview#file-protocol
+ return {
+ filepath: savedFile.uri,
+ webviewPath: Capacitor.convertFileSrc(savedFile.uri),
+ };
+ } else {
+ // Use webPath to display the new image instead of base64 since it's
+ // already loaded into memory
+ return {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ };
+ }
+ };
+
+ useEffect(() => {
+ const loadSaved = async () => {
+ const { value } = await Preferences.get({ key: PHOTO_STORAGE });
+
+ const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
+ // If running on the web...
+ if (!isPlatform('hybrid')) {
+ for (let photo of photosInPreferences) {
+ const file = 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}`;
+ }
+ }
+ setPhotos(photosInPreferences);
+ };
+ }, []);
+
+ const takePhoto = async () => {
+ const photo = await Camera.getPhoto({
+ resultType: CameraResultType.Uri,
+ source: CameraSource.Camera,
+ quality: 100,
+ });
+
+ const newPhotos = [
+ {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ },
+ ...photos,
+ ];
+ setPhotos(newPhotos);
+ Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
+ };
+
+ return {
+ photos,
+ takePhoto,
+ };
+}
+
+export async function base64FromPath(path: string): Promise {
+ const response = await fetch(path);
+ const blob = await response.blob();
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onerror = reject;
+ reader.onload = () => {
+ if (typeof reader.result === 'string') {
+ resolve(reader.result);
+ } else {
+ reject('method did not return a string');
+ }
+ };
+ reader.readAsDataURL(blob);
+ });
+}
+
+export interface UserPhoto {
+ filepath: string;
+ webviewPath?: string;
+}
+```
diff --git a/docs/react/your-first-app/7-live-reload.md b/docs/react/your-first-app/7-live-reload.md
index 023da7a8844..107cf3397eb 100644
--- a/docs/react/your-first-app/7-live-reload.md
+++ b/docs/react/your-first-app/7-live-reload.md
@@ -29,27 +29,78 @@ The Live Reload server will start up, and the native IDE of choice will open if
With Live Reload running and the app is open on your device, let’s implement photo deletion functionality. Open `Tab2.tsx` then import `useState` from React and `UserPhoto` from the `usePhotoGallery` hook:
```tsx
+// Other Imports
+
+// CHANGE: Import UserPhoto, usePhotoGallery hook and useState from react.
import React, { useState } from 'react';
import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery';
-// other imports
+
+const Tab2: React.FC = () => {};
```
Next, reference the `deletePhoto` function, which we'll create soon:
```tsx
-const { photos, takePhoto, deletePhoto } = usePhotoGallery();
+// Same old code from before.
+
+const Tab2: React.FC = () => {
+ // CHANGE: Reference deletePhoto function
+ const { photos, takePhoto, deletePhoto } = usePhotoGallery();
+
+ // Same old code from before.
+};
```
Next, add a state value to store information about the photo to delete:
```tsx
-const [photoToDelete, setPhotoToDelete] = useState();
+// Same old code from before.
+
+const Tab2: React.FC = () => {
+ // Same old code from before.
+
+ // CHANGE: Add a state value for photo deletion.
+ const [photoToDelete, setPhotoToDelete] = useState();
+
+ // Same old code from before.
+};
```
When a user clicks on an image, we will show the action sheet by changing the state value to the photo. Update the `` element to:
```tsx
- setPhotoToDelete(photo)} src={photo.webviewPath} />
+// Same old code from before.
+
+const Tab2: React.FC = () => {
+ // Same old code from before.
+
+ return (
+
+
+
+ Tab 2
+
+
+
+
+
+ {photos.map((photo, index) => (
+
+
+ setPhotoToDelete(photo)} src={photo.webviewPath} />
+
+ ))}
+
+
+
+ takePhoto()}>
+
+
+
+
+
+ );
+};
```
Next, add an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet) dialog with the option to either delete the selected photo or cancel (close) the dialog. We will set the isOpen property based on if photoToDelete has a value or not.
@@ -57,28 +108,60 @@ Next, add an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet)
In the JSX, put the following component before the closing `` tag.
```tsx
- {
- if (photoToDelete) {
- deletePhoto(photoToDelete);
- setPhotoToDelete(undefined);
- }
- },
- },
- {
- text: 'Cancel',
- icon: close,
- role: 'cancel',
- },
- ]}
- onDidDismiss={() => setPhotoToDelete(undefined)}
-/>
+// Same old code from before.
+
+const Tab2: React.FC = () => {
+ // Same old code from before.
+
+ return (
+
+
+
+ Tab 2
+
+
+
+
+
+ {photos.map((photo, index) => (
+
+ setPhotoToDelete(photo)} src={photo.webviewPath} />
+
+ ))}
+
+
+
+ takePhoto()}>
+
+
+
+
+ {
+ if (photoToDelete) {
+ deletePhoto(photoToDelete);
+ setPhotoToDelete(undefined);
+ }
+ },
+ },
+ {
+ text: 'Cancel',
+ icon: close,
+ role: 'cancel',
+ },
+ ]}
+ onDidDismiss={() => setPhotoToDelete(undefined)}
+ />
+
+
+ );
+};
```
Above, we added two options: `Delete` that calls `deletePhoto` function (to be added next) and `Cancel`, which when given the role of “cancel” will automatically close the action sheet. It's also important to set the onDidDismiss function and set our photoToDelete back to undefined when the modal goes away. That way, when another image is clicked, the action sheet notices the change in the value of photoToDelete.
@@ -86,35 +169,250 @@ Above, we added two options: `Delete` that calls `deletePhoto` function (to be a
Next, we need to implement the deletePhoto method that will come from the `usePhotoGallery` hook. Open the file and paste in the following function in the hook:
```tsx
-const deletePhoto = async (photo: UserPhoto) => {
- // Remove this photo from the Photos reference data array
- const newPhotos = photos.filter((p) => p.filepath !== photo.filepath);
-
- // Update photos array cache by overwriting the existing photo array
- Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
-
- // delete photo file from filesystem
- const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1);
- await Filesystem.deleteFile({
- path: filename,
- directory: Directory.Data,
- });
- setPhotos(newPhotos);
-};
+// Same old code from before.
+
+export function usePhotoGallery() {
+ // Same old code from before.
+
+ // CHANGE: Implement deletePhoto method within usePhotoGallery hook.
+ const deletePhoto = async (photo: UserPhoto) => {
+ // Remove this photo from the Photos reference data array
+ const newPhotos = photos.filter((p) => p.filepath !== photo.filepath);
+
+ // Update photos array cache by overwriting the existing photo array
+ Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
+
+ // delete photo file from filesystem
+ const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1);
+ await Filesystem.deleteFile({
+ path: filename,
+ directory: Directory.Data,
+ });
+ setPhotos(newPhotos);
+ };
+
+ // CHANGE: Update return and add deletePhoto function
+ return {
+ photos,
+ takePhoto,
+ deletePhoto,
+ };
+}
+
+// Same old code from before.
```
The selected photo is removed from the Photos array first. Then, we use the Capacitor Preferences API to update the cached version of the Photos array. Finally, we delete the actual photo file itself using the Filesystem API.
-Make sure to return the `deletePhoto` function so it is as a part of the hook API that we expose:
+Save this file, then tap on a photo again and choose the “Delete” option. This time, the photo is deleted! Implemented much faster using Live Reload. 💪
+
+In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices.
+
+`Tab2.tsx` should look like this:
```tsx
-return {
- photos,
- takePhoto,
- deletePhoto,
+import { camera, trash, close } from 'ionicons/icons';
+import {
+ IonContent,
+ IonHeader,
+ IonPage,
+ IonTitle,
+ IonToolbar,
+ IonFab,
+ IonFabButton,
+ IonIcon,
+ IonGrid,
+ IonRow,
+ IonCol,
+ IonImg,
+ IonActionSheet,
+} from '@ionic/react';
+import ExploreContainer from '../components/ExploreContainer';
+import './Tab2.css';
+import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery';
+
+const Tab2: React.FC = () => {
+ const { photos, takePhoto, deletePhoto } = usePhotoGallery();
+ const [photoToDelete, setPhotoToDelete] = useState();
+
+ return (
+
+
+
+ Tab 2
+
+
+
+
+
+ {photos.map((photo, index) => (
+
+ setPhotoToDelete(photo)} src={photo.webviewPath} />
+
+ ))}
+
+
+
+ takePhoto()}>
+
+
+
+ {
+ if (photoToDelete) {
+ deletePhoto(photoToDelete);
+ setPhotoToDelete(undefined);
+ }
+ },
+ },
+ {
+ text: 'Cancel',
+ icon: close,
+ role: 'cancel',
+ },
+ ]}
+ onDidDismiss={() => setPhotoToDelete(undefined)}
+ />
+
+
+ );
};
+
+export default Tab2;
```
-Save this file, then tap on a photo again and choose the “Delete” option. This time, the photo is deleted! Implemented much faster using Live Reload. 💪
+`usePhotoGallery.ts` should look like this:
-In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices.
+```tsx
+import { useState, useEffect } from 'react';
+import { isPlatform } from '@ionic/react';
+import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
+import { Filesystem, Directory } from '@capacitor/filesystem';
+import { Preferences } from '@capacitor/preferences';
+import { Capacitor } from '@capacitor/core';
+
+const PHOTO_STORAGE = 'photos';
+
+export function usePhotoGallery() {
+ const [photos, setPhotos] = useState([]);
+
+ const savePicture = async (photo: Photo, fileName: string): Promise => {
+ let base64Data: string | Blob;
+ // "hybrid" will detect Cordova or Capacitor:
+ if (isPlatform('hybrid')) {
+ const file = await Filesystem.readFile({
+ path: photo.path!,
+ });
+ base64Data = file.data;
+ } else {
+ base64Data = await base64FromPath(photo.webPath!);
+ }
+
+ const savedFile = await Filesystem.writeFile({
+ path: fileName,
+ data: base64Data,
+ directory: Directory.Data,
+ });
+
+ if (isPlatform('hybrid')) {
+ // Display the new image by rewriting the 'file://' path to HTTP
+ // Details: https://ionicframework.com/docs/building/webview#file-protocol
+ return {
+ filepath: savedFile.uri,
+ webviewPath: Capacitor.convertFileSrc(savedFile.uri),
+ };
+ } else {
+ // Use webPath to display the new image instead of base64 since it's
+ // already loaded into memory
+ return {
+ filepath: fileName,
+ webviewPath: photo.webPath,
+ };
+ }
+ };
+
+ useEffect(() => {
+ const loadSaved = async () => {
+ const { value } = await Preferences.get({ key: PHOTO_STORAGE });
+ const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[];
+
+ if (!isPlatform('hybrid')) {
+ for (let photo of photosInPreferences) {
+ const file = 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}`;
+ }
+ }
+ setPhotos(photosInPreferences);
+ };
+ loadSaved();
+ }, []);
+
+ const takePhoto = async () => {
+ const photo = await Camera.getPhoto({
+ resultType: CameraResultType.Uri,
+ source: CameraSource.Camera,
+ quality: 100,
+ });
+
+ const fileName = Date.now() + '.jpeg';
+ const savedFileImage = await savePicture(photo, fileName);
+ const newPhotos = [savedFileImage, ...photos];
+ setPhotos(newPhotos);
+ Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
+ };
+
+ const deletePhoto = async (photo: UserPhoto) => {
+ // Remove this photo from the Photos reference data array
+ const newPhotos = photos.filter((p) => p.filepath !== photo.filepath);
+
+ // Update photos array cache by overwriting the existing photo array
+ Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) });
+
+ // delete photo file from filesystem
+ const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1);
+ await Filesystem.deleteFile({
+ path: filename,
+ directory: Directory.Data,
+ });
+ setPhotos(newPhotos);
+ };
+
+ return {
+ photos,
+ takePhoto,
+ deletePhoto,
+ };
+}
+
+export async function base64FromPath(path: string): Promise {
+ const response = await fetch(path);
+ const blob = await response.blob();
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onerror = reject;
+ reader.onload = () => {
+ if (typeof reader.result === 'string') {
+ resolve(reader.result);
+ } else {
+ reject('method did not return a string');
+ }
+ };
+ reader.readAsDataURL(blob);
+ });
+}
+
+export interface UserPhoto {
+ filepath: string;
+ webviewPath?: string;
+}
+```