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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 48 additions & 8 deletions docs/angular/your-first-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,16 @@ npm install @ionic/pwa-elements
Next, import `@ionic/pwa-elements` by editing `src/main.ts`.

```tsx
import { defineCustomElements } from '@ionic/pwa-elements/loader';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { defineCustomElements } from '@ionic/pwa-elements/loader'; // Added import

// Call the element loader before the bootstrapModule/bootstrapApplication call
defineCustomElements(window);

platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.log(err));
```

That’s it! Now for the fun part - let’s see the app in action.
Expand All @@ -137,18 +143,20 @@ There are three tabs. Click on the Tab2 tab. It’s a blank canvas, aka the perf
Open the photo-gallery app folder in your code editor of choice, then navigate to `/src/app/tab2/tab2.page.html`. We see:

```html
<ion-header>
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>Tab 2</ion-title>
<ion-title> Tab 2 </ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tab 2</ion-title>
</ion-toolbar>
</ion-header>

<app-explore-container name="Tab 2 page"></app-explore-container>
</ion-content>
```

Expand All @@ -161,22 +169,54 @@ Open the photo-gallery app folder in your code editor of choice, then navigate t
We put the visual aspects of our app into `<ion-content>`. 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.

```html
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title> Tab 2 </ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tab 2</ion-title>
</ion-toolbar>
</ion-header>

<!-- add floating action button -->
<ion-fab vertical="bottom" horizontal="center" slot="fixed">
<ion-fab-button>
<ion-icon name="camera"></ion-icon>
</ion-fab-button>
</ion-fab>

<!-- remove app-explore-container -->
<!-- <app-explore-container name="Tab 2 page"></app-explore-container> -->
</ion-content>
```

Next, open `src/app/tabs/tabs.page.html`. Change the label to “Photos” and the icon name to “images”:

```html
<ion-tab-button tab="tab2">
<ion-icon name="images"></ion-icon>
<ion-label>Photos</ion-label>
</ion-tab-button>
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab1" href="/tabs/tab1">
<ion-icon aria-hidden="true" name="triangle"></ion-icon>
<ion-label>Tab 1</ion-label>
</ion-tab-button>

<ion-tab-button tab="tab2" href="/tabs/tab2">
<!-- update icon-->
<ion-icon aria-hidden="true" name="images"></ion-icon>
<!-- update label -->
<ion-label>Photos</ion-label>
</ion-tab-button>

<ion-tab-button tab="tab3" href="/tabs/tab3">
<ion-icon aria-hidden="true" name="square"></ion-icon>
<ion-label>Tab 3</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
```

Save all changes to see them automatically applied in the browser. 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.
125 changes: 118 additions & 7 deletions docs/angular/your-first-app/2-taking-photos.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,72 @@ public async addNewToGallery() {

Notice the magic here: there's no platform-specific code (web, iOS, or Android)! The Capacitor Camera plugin abstracts that away for us, leaving just one method call - `Camera.getPhoto()` - that will open up the device's camera and allow us to take photos.

Your updated `photo.service.ts` should now look like this:

```tsx
import { Injectable } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';

@Injectable({
providedIn: 'root',
})
export class PhotoService {
constructor() {}

public async addNewToGallery() {
// Take a photo
const capturedPhoto = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 100,
});
}
}
```

Next, open up `tab2.page.ts` and import the PhotoService class and add a method that calls the `addNewToGallery` method on the imported service:

```tsx
import { Component } from '@angular/core';
import { PhotoService } from '../services/photo.service';

constructor(public photoService: PhotoService) { }

addPhotoToGallery() {
this.photoService.addNewToGallery();
@Component({
selector: 'app-tab2',
templateUrl: 'tab2.page.html',
styleUrls: ['tab2.page.scss'],
standalone: false,
})
export class Tab2Page {
// update constructor to include photoService
constructor(public photoService: PhotoService) {}

// add addNewToGallery method
addPhotoToGallery() {
this.photoService.addNewToGallery();
}
}
```

Then, open `tab2.page.html` and call the `addPhotoToGallery()` function when the FAB is tapped/clicked:

```html
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title> Tab 2 </ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tab 2</ion-title>
</ion-toolbar>
</ion-header>

<ion-fab vertical="bottom" horizontal="center" slot="fixed">
<!-- add click event listener to floating action button -->
<ion-fab-button (click)="addPhotoToGallery()">
<ion-icon name="camera"></ion-icon>
</ion-fab-button>
Expand All @@ -78,7 +127,7 @@ After taking a photo, it disappears right away. We need to display it within our

## Displaying Photos

Outside of the `PhotoService` class definition (the very bottom of the file), create a new interface, `UserPhoto`, to hold our photo metadata:
Return to `photo.service.ts`. Outside of the `PhotoService` class definition (the very bottom of the file), create a new interface, `UserPhoto`, to hold our photo metadata:

```tsx
export interface UserPhoto {
Expand All @@ -87,48 +136,110 @@ export interface UserPhoto {
}
```

Back at the top of the file, define an array of Photos, which will contain a reference to each photo captured with the Camera.
Back at the top of the `PhotoService` class definition, define an array of Photos, which will contain a reference to each photo captured with the Camera.

```tsx
export class PhotoService {
public photos: UserPhoto[] = [];

constructor() {}

// other code
}
```

Over in the `addNewToGallery` function, add the newly captured photo to the beginning of the Photos array.

```tsx
public async addNewToGallery() {
const capturedPhoto = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 100
});

// add new photo to photos array
this.photos.unshift({
filepath: "soon...",
webviewPath: capturedPhoto.webPath!
});
}
```

`photo.service.ts` should now look like this:

```tsx
import { Injectable } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';

@Injectable({
providedIn: 'root',
})
export class PhotoService {
public photos: UserPhoto[] = [];
constructor() {}

public async addNewToGallery() {
const capturedPhoto = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 100,
});

// add new photo to photos array
this.photos.unshift({
filepath: 'soon...',
webviewPath: capturedPhoto.webPath!,
});
}
}

export interface UserPhoto {
filepath: string;
webviewPath?: string;
}
```

Next, move over to `tab2.page.html` so we can display the image 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 `PhotoServices`'s Photos array, adding an Image component (`<ion-img>`) for each. Point the `src` (source) at the photo’s path:

```html
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title> Tab 2 </ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Tab 2</ion-title>
</ion-toolbar>
</ion-header>

<!-- add grid component to display array of photos -->
<ion-grid>
<ion-row>
<!-- create new column and image component for each photo -->
<ion-col size="6" *ngFor="let photo of photoService.photos; index as position">
<ion-img [src]="photo.webviewPath"></ion-img>
</ion-col>
</ion-row>
</ion-grid>

<!-- ion-fab markup -->
<ion-fab vertical="bottom" horizontal="center" slot="fixed">
<ion-fab-button (click)="addPhotoToGallery()">
<ion-icon name="camera"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>
```

:::note
Learn more about the [ngFor core directive](https://blog.angular-university.io/angular-2-ngfor/).
:::

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!

Up next, we’ll add support for saving the photos to the filesystem, so they can be retrieved and displayed in our app at a later time.
79 changes: 77 additions & 2 deletions docs/angular/your-first-app/3-saving-photos.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public async addNewToGallery() {

// Save the picture and add it to photo collection
const savedImageFile = await this.savePicture(capturedPhoto);
// update argument to unshift array method
this.photos.unshift(savedImageFile);
}
```
Expand Down Expand Up @@ -55,7 +56,7 @@ private async savePicture(photo: Photo) {
}
```

`readAsBase64()` is a helper function we’ll define next. It's useful to organize via a separate method since it requires a small amount of platform-specific (web vs. mobile) logic - more on that in a bit. For now, implement the logic for running on the web:
`readAsBase64()` is a helper function we’ll define next. It's useful to organize via a separate method since it requires a small amount of platform-specific (web vs. mobile) logic - more on that in a bit. For now, we'll create two new helper functions, `readAsBase64()` and `convertBlobToBase64()`, to implement the logic for running on the web:

```tsx
private async readAsBase64(photo: Photo) {
Expand All @@ -76,6 +77,80 @@ private convertBlobToBase64 = (blob: Blob) => new Promise((resolve, reject) => {
});
```

`photo.service.ts` should now look like this:

```tsx
import { Injectable } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';

@Injectable({
providedIn: 'root',
})
export class PhotoService {
public photos: UserPhoto[] = [];
constructor() {}

public async addNewToGallery() {
const capturedPhoto = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 100,
});

// Save the picture and add it to photo collection
const savedImageFile = await this.savePicture(capturedPhoto);
// update argument to unshift array method
this.photos.unshift(savedImageFile);
}

private async savePicture(photo: Photo) {
// Convert photo to base64 format, required by Filesystem API to save
const base64Data = await this.readAsBase64(photo);

// Write the file to the data directory
const fileName = Date.now() + '.jpeg';
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,
};
}

private async readAsBase64(photo: Photo) {
// Fetch the photo, read as a blob, then convert to base64 format
const response = await fetch(photo.webPath!);
const blob = await response.blob();

return (await this.convertBlobToBase64(blob)) as string;
}

private convertBlobToBase64 = (blob: Blob) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(blob);
});
}

export interface UserPhoto {
filepath: string;
webviewPath?: string;
}
```

Obtaining the camera photo as base64 format on the web appears to be a bit trickier than on mobile. In reality, we’re just using built-in web APIs: [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) as a neat way to read the file into blob format, then FileReader’s [readAsDataURL()](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL) to convert the photo blob to base64.

There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem.
There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. Next up, we'll load and display our saved images
when the user navigates to the Photos tab.
Loading
Loading