Skip to content

Ionic Cheatsheet

Ionic - Cross-Platform Mobile Apps with Web Technologies

Ionic is an open-source mobile app development framework that enables developers to build high-quality cross-platform mobile apps using web technologies like HTML, CSS, and JavaScript. It provides native mobile components and tools for building hybrid mobile applications.

Table of Contents

Installation

Prerequisites

# Install Node.js (LTS version recommended)
# Download from https://nodejs.org/

# Install npm or yarn
npm --version
yarn --version

Ionic CLI Installation

# Install Ionic CLI globally
npm install -g @ionic/cli

# Verify installation
ionic --version

# Install Cordova CLI (for Cordova integration)
npm install -g cordova

# Install Capacitor CLI (for Capacitor integration)
npm install -g @capacitor/cli

Development Environment Setup

# For Android development
# Install Android Studio from https://developer.android.com/studio
# Set up Android SDK and emulator

# For iOS development (macOS only)
# Install Xcode from Mac App Store
# Install CocoaPods
sudo gem install cocoapods

Getting Started

Create New Project

# Create a new Ionic project
ionic start myApp tabs --type=angular

# Available templates:
# - blank: A blank starter project
# - tabs: A starting project with a simple tabbed interface
# - sidemenu: A starting project with a side menu with navigation

# Framework options:
# - angular (default)
# - react
# - vue

# Create with specific framework
ionic start myApp tabs --type=react
ionic start myApp tabs --type=vue

# Navigate to project directory
cd myApp

# Serve the app in development mode
ionic serve

# Serve with specific port
ionic serve --port=8100

# Serve with lab mode (iOS and Android preview)
ionic serve --lab

Project Structure

myApp/
├── src/
│   ├── app/
│   │   ├── pages/          # Page components
│   │   ├── components/     # Reusable components
│   │   ├── services/       # Services and providers
│   │   ├── models/         # Data models
│   │   └── app.module.ts   # Main app module
│   ├── assets/             # Static assets
│   ├── theme/              # Global styles
│   └── index.html          # Main HTML file
├── android/                # Android platform files
├── ios/                    # iOS platform files
├── www/                    # Built web assets
├── ionic.config.json       # Ionic configuration
├── capacitor.config.json   # Capacitor configuration
└── package.json            # Dependencies

Basic App Setup (Angular)

// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule
  ],
  providers: [
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

// src/app/app.component.ts
import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {
  constructor(private platform: Platform) {
    this.initializeApp();
  }

  initializeApp() {
    this.platform.ready().then(() => {
      console.log('Platform is ready');
    });
  }
}

Components

Basic Components

// Home page component
import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  title = 'Welcome to Ionic';
  items = [
    { id: 1, name: 'Item 1', description: 'Description 1' },
    { id: 2, name: 'Item 2', description: 'Description 2' },
    { id: 3, name: 'Item 3', description: 'Description 3' },
  ];

  constructor() {}

  onItemClick(item: any) {
    console.log('Item clicked:', item);
  }

  doRefresh(event: any) {
    setTimeout(() => {
      console.log('Async operation has ended');
      event.target.complete();
    }, 2000);
  }
}
<!-- home.page.html -->
<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>{{ title }}</ion-title>
    <ion-buttons slot="end">
      <ion-button>
        <ion-icon name="settings-outline"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <!-- Refresher -->
  <ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
    <ion-refresher-content></ion-refresher-content>
  </ion-refresher>

  <!-- Header -->
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">{{ title }}</ion-title>
    </ion-toolbar>
  </ion-header>

  <!-- Content -->
  <div class="ion-padding">
    <!-- Cards -->
    <ion-card>
      <ion-card-header>
        <ion-card-title>Card Title</ion-card-title>
        <ion-card-subtitle>Card Subtitle</ion-card-subtitle>
      </ion-card-header>
      <ion-card-content>
        This is the card content. You can put any content here.
      </ion-card-content>
    </ion-card>

    <!-- List -->
    <ion-list>
      <ion-list-header>
        <ion-label>Items</ion-label>
      </ion-list-header>
      <ion-item *ngFor="let item of items" (click)="onItemClick(item)">
        <ion-avatar slot="start">
          <img src="https://via.placeholder.com/40" />
        </ion-avatar>
        <ion-label>
          <h2>{{ item.name }}</h2>
          <p>{{ item.description }}</p>
        </ion-label>
        <ion-icon name="chevron-forward-outline" slot="end"></ion-icon>
      </ion-item>
    </ion-list>

    <!-- Buttons -->
    <ion-button expand="block" fill="solid" color="primary">
      Primary Button
    </ion-button>

    <ion-button expand="block" fill="outline" color="secondary">
      Secondary Button
    </ion-button>

    <!-- Input -->
    <ion-item>
      <ion-label position="floating">Email</ion-label>
      <ion-input type="email" placeholder="Enter email"></ion-input>
    </ion-item>

    <!-- Checkbox -->
    <ion-item>
      <ion-checkbox slot="start"></ion-checkbox>
      <ion-label>Accept terms and conditions</ion-label>
    </ion-item>

    <!-- Toggle -->
    <ion-item>
      <ion-label>Enable notifications</ion-label>
      <ion-toggle slot="end"></ion-toggle>
    </ion-item>

    <!-- Range -->
    <ion-item>
      <ion-label>Volume</ion-label>
      <ion-range min="0" max="100" value="50" slot="end">
        <ion-icon name="volume-low" slot="start"></ion-icon>
        <ion-icon name="volume-high" slot="end"></ion-icon>
      </ion-range>
    </ion-item>
  </div>

  <!-- Floating Action Button -->
  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button>
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>

Advanced Components

// Modal component
import { Component, Input } from '@angular/core';
import { ModalController } from '@ionic/angular';

@Component({
  selector: 'app-modal',
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-title>Modal</ion-title>
        <ion-buttons slot="end">
          <ion-button (click)="dismiss()">Close</ion-button>
        </ion-buttons>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      <div class="ion-padding">
        <p>{{ data }}</p>
      </div>
    </ion-content>
  `,
})
export class ModalComponent {
  @Input() data: string = '';

  constructor(private modalController: ModalController) {}

  dismiss() {
    this.modalController.dismiss();
  }
}

// Using modal in a page
import { ModalController } from '@ionic/angular';
import { ModalComponent } from './modal.component';

export class HomePage {
  constructor(private modalController: ModalController) {}

  async presentModal() {
    const modal = await this.modalController.create({
      component: ModalComponent,
      componentProps: {
        data: 'Hello from modal!'
      }
    });
    return await modal.present();
  }
}

// Popover component
import { Component } from '@angular/core';
import { PopoverController } from '@ionic/angular';

@Component({
  selector: 'app-popover',
  template: `
    <ion-content>
      <ion-list>
        <ion-item button (click)="close('option1')">
          <ion-label>Option 1</ion-label>
        </ion-item>
        <ion-item button (click)="close('option2')">
          <ion-label>Option 2</ion-label>
        </ion-item>
      </ion-list>
    </ion-content>
  `,
})
export class PopoverComponent {
  constructor(private popoverController: PopoverController) {}

  close(data?: any) {
    this.popoverController.dismiss(data);
  }
}

// Action Sheet
import { ActionSheetController } from '@ionic/angular';

export class HomePage {
  constructor(private actionSheetController: ActionSheetController) {}

  async presentActionSheet() {
    const actionSheet = await this.actionSheetController.create({
      header: 'Albums',
      cssClass: 'my-custom-class',
      buttons: [
        {
          text: 'Delete',
          role: 'destructive',
          icon: 'trash',
          handler: () => {
            console.log('Delete clicked');
          }
        },
        {
          text: 'Share',
          icon: 'share',
          handler: () => {
            console.log('Share clicked');
          }
        },
        {
          text: 'Cancel',
          icon: 'close',
          role: 'cancel',
          handler: () => {
            console.log('Cancel clicked');
          }
        }
      ]
    });
    await actionSheet.present();
  }
}

// Alert
import { AlertController } from '@ionic/angular';

export class HomePage {
  constructor(private alertController: AlertController) {}

  async presentAlert() {
    const alert = await this.alertController.create({
      header: 'Alert',
      subHeader: 'Subtitle',
      message: 'This is an alert message.',
      buttons: ['OK']
    });
    await alert.present();
  }

  async presentConfirm() {
    const alert = await this.alertController.create({
      header: 'Confirm!',
      message: 'Are you sure you want to delete this item?',
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel',
          cssClass: 'secondary',
          handler: () => {
            console.log('Confirm Cancel');
          }
        },
        {
          text: 'Okay',
          handler: () => {
            console.log('Confirm Okay');
          }
        }
      ]
    });
    await alert.present();
  }
}

// Toast
import { ToastController } from '@ionic/angular';

export class HomePage {
  constructor(private toastController: ToastController) {}

  async presentToast() {
    const toast = await this.toastController.create({
      message: 'Your settings have been saved.',
      duration: 2000,
      position: 'bottom'
    });
    toast.present();
  }
}

// Loading
import { LoadingController } from '@ionic/angular';

export class HomePage {
  constructor(private loadingController: LoadingController) {}

  async presentLoading() {
    const loading = await this.loadingController.create({
      message: 'Please wait...',
      duration: 2000
    });
    await loading.present();

    const { role, data } = await loading.onDidDismiss();
    console.log('Loading dismissed!');
  }
}

Router Navigation

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'home',
    loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
  },
  {
    path: 'details/:id',
    loadChildren: () => import('./details/details.module').then(m => m.DetailsPageModule)
  },
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
  },
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

// Navigation in component
import { Router } from '@angular/router';
import { NavController } from '@ionic/angular';

export class HomePage {
  constructor(
    private router: Router,
    private navController: NavController
  ) {}

  // Using Angular Router
  navigateToDetails(id: number) {
    this.router.navigate(['/details', id]);
  }

  // Using Ionic NavController
  navigateWithNavController() {
    this.navController.navigateForward('/details/123');
  }

  goBack() {
    this.navController.back();
  }
}

Tab Navigation

// tabs.page.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-tabs',
  templateUrl: 'tabs.page.html',
  styleUrls: ['tabs.page.scss']
})
export class TabsPage {
  constructor() {}
}
<!-- tabs.page.html -->
<ion-tabs>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="home">
      <ion-icon name="home"></ion-icon>
      <ion-label>Home</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="search">
      <ion-icon name="search"></ion-icon>
      <ion-label>Search</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="profile">
      <ion-icon name="person"></ion-icon>
      <ion-label>Profile</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

Side Menu Navigation

// app.component.ts
import { Component } from '@angular/core';
import { MenuController } from '@ionic/angular';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {
  public appPages = [
    { title: 'Home', url: '/home', icon: 'home' },
    { title: 'Profile', url: '/profile', icon: 'person' },
    { title: 'Settings', url: '/settings', icon: 'settings' },
  ];

  constructor(private menu: MenuController) {}

  openMenu() {
    this.menu.enable(true, 'first');
    this.menu.open('first');
  }

  closeMenu() {
    this.menu.close();
  }
}
<!-- app.component.html -->
<ion-app>
  <ion-split-pane contentId="main-content">
    <ion-menu contentId="main-content" type="overlay">
      <ion-content>
        <ion-list id="inbox-list">
          <ion-list-header>Menu</ion-list-header>
          <ion-menu-toggle auto-hide="false" *ngFor="let p of appPages; let i = index">
            <ion-item routerDirection="root" [routerLink]="[p.url]" lines="none" detail="false">
              <ion-icon slot="start" [name]="p.icon"></ion-icon>
              <ion-label>{{ p.title }}</ion-label>
            </ion-item>
          </ion-menu-toggle>
        </ion-list>
      </ion-content>
    </ion-menu>
    <ion-router-outlet id="main-content"></ion-router-outlet>
  </ion-split-pane>
</ion-app>

Styling

CSS Variables and Theming

// src/theme/variables.scss
:root {
  /** Primary colors **/
  --ion-color-primary: #3880ff;
  --ion-color-primary-rgb: 56, 128, 255;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255, 255, 255;
  --ion-color-primary-shade: #3171e0;
  --ion-color-primary-tint: #4c8dff;

  /** Secondary colors **/
  --ion-color-secondary: #3dc2ff;
  --ion-color-secondary-rgb: 61, 194, 255;
  --ion-color-secondary-contrast: #ffffff;
  --ion-color-secondary-contrast-rgb: 255, 255, 255;
  --ion-color-secondary-shade: #36abe0;
  --ion-color-secondary-tint: #50c8ff;

  /** Custom colors **/
  --ion-color-custom: #ff6b6b;
  --ion-color-custom-rgb: 255, 107, 107;
  --ion-color-custom-contrast: #ffffff;
  --ion-color-custom-contrast-rgb: 255, 255, 255;
  --ion-color-custom-shade: #e05e5e;
  --ion-color-custom-tint: #ff7a7a;
}

.ion-color-custom {
  --ion-color-base: var(--ion-color-custom);
  --ion-color-base-rgb: var(--ion-color-custom-rgb);
  --ion-color-contrast: var(--ion-color-custom-contrast);
  --ion-color-contrast-rgb: var(--ion-color-custom-contrast-rgb);
  --ion-color-shade: var(--ion-color-custom-shade);
  --ion-color-tint: var(--ion-color-custom-tint);
}

// Dark mode
@media (prefers-color-scheme: dark) {
  :root {
    --ion-color-primary: #428cff;
    --ion-color-primary-rgb: 66, 140, 255;
    --ion-color-primary-contrast: #ffffff;
    --ion-color-primary-contrast-rgb: 255, 255, 255;
    --ion-color-primary-shade: #3a7be0;
    --ion-color-primary-tint: #5598ff;
  }
}

Component Styling

// home.page.scss
.welcome-card {
  ion-card-header {
    background: linear-gradient(135deg, var(--ion-color-primary), var(--ion-color-secondary));
    color: white;
  }

  ion-card-title {
    font-size: 1.5rem;
    font-weight: bold;
  }
}

.custom-button {
  --background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
  --border-radius: 25px;
  --padding-start: 2rem;
  --padding-end: 2rem;
  margin: 1rem 0;
}

.item-list {
  ion-item {
    --border-color: var(--ion-color-light-shade);
    --padding-start: 1rem;

    &:hover {
      --background: var(--ion-color-light);
    }
  }
}

// Responsive design
@media (min-width: 768px) {
  .desktop-only {
    display: block;
  }

  .mobile-only {
    display: none;
  }
}

@media (max-width: 767px) {
  .desktop-only {
    display: none;
  }

  .mobile-only {
    display: block;
  }
}

Global Styles

// src/global.scss
// Custom utility classes
.text-center {
  text-align: center;
}

.margin-top {
  margin-top: 1rem;
}

.padding-horizontal {
  padding-left: 1rem;
  padding-right: 1rem;
}

// Custom animations
@keyframes slideIn {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.slide-in {
  animation: slideIn 0.3s ease-in-out;
}

// Custom scrollbar
::-webkit-scrollbar {
  width: 8px;
}

::-webkit-scrollbar-track {
  background: var(--ion-color-light);
}

::-webkit-scrollbar-thumb {
  background: var(--ion-color-medium);
  border-radius: 4px;
}

Native Features

Camera

// Install Capacitor Camera plugin
// npm install @capacitor/camera

import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';

export class CameraService {
  async takePicture() {
    try {
      const image = await Camera.getPhoto({
        quality: 90,
        allowEditing: true,
        resultType: CameraResultType.Uri,
        source: CameraSource.Camera
      });

      return image.webPath;
    } catch (error) {
      console.error('Error taking picture:', error);
      throw error;
    }
  }

  async selectFromGallery() {
    try {
      const image = await Camera.getPhoto({
        quality: 90,
        allowEditing: true,
        resultType: CameraResultType.Uri,
        source: CameraSource.Photos
      });

      return image.webPath;
    } catch (error) {
      console.error('Error selecting from gallery:', error);
      throw error;
    }
  }
}

Geolocation

// Install Capacitor Geolocation plugin
// npm install @capacitor/geolocation

import { Geolocation } from '@capacitor/geolocation';

export class LocationService {
  async getCurrentPosition() {
    try {
      const coordinates = await Geolocation.getCurrentPosition();
      return {
        latitude: coordinates.coords.latitude,
        longitude: coordinates.coords.longitude,
        accuracy: coordinates.coords.accuracy
      };
    } catch (error) {
      console.error('Error getting location:', error);
      throw error;
    }
  }

  async watchPosition() {
    const watchId = await Geolocation.watchPosition({
      enableHighAccuracy: true,
      timeout: 10000
    }, (position, err) => {
      if (err) {
        console.error('Error watching position:', err);
        return;
      }

      console.log('Position updated:', position);
    });

    return watchId;
  }

  async clearWatch(watchId: string) {
    await Geolocation.clearWatch({ id: watchId });
  }
}

Device Information

// Install Capacitor Device plugin
// npm install @capacitor/device

import { Device } from '@capacitor/device';

export class DeviceService {
  async getDeviceInfo() {
    try {
      const info = await Device.getInfo();
      return {
        model: info.model,
        platform: info.platform,
        operatingSystem: info.operatingSystem,
        osVersion: info.osVersion,
        manufacturer: info.manufacturer,
        isVirtual: info.isVirtual,
        webViewVersion: info.webViewVersion
      };
    } catch (error) {
      console.error('Error getting device info:', error);
      throw error;
    }
  }

  async getBatteryInfo() {
    try {
      const info = await Device.getBatteryInfo();
      return {
        batteryLevel: info.batteryLevel,
        isCharging: info.isCharging
      };
    } catch (error) {
      console.error('Error getting battery info:', error);
      throw error;
    }
  }
}

Push Notifications

// Install Capacitor Push Notifications plugin
// npm install @capacitor/push-notifications

import { PushNotifications } from '@capacitor/push-notifications';

export class PushNotificationService {
  async initializePushNotifications() {
    // Request permission to use push notifications
    await PushNotifications.requestPermissions();

    // Register with Apple / Google to receive push via APNS/FCM
    await PushNotifications.register();

    // On success, we should be able to receive notifications
    PushNotifications.addListener('registration', (token) => {
      console.log('Push registration success, token: ' + token.value);
    });

    // Some issue with our setup and push will not work
    PushNotifications.addListener('registrationError', (error) => {
      console.error('Error on registration: ' + JSON.stringify(error));
    });

    // Show us the notification payload if the app is open on our device
    PushNotifications.addListener('pushNotificationReceived', (notification) => {
      console.log('Push received: ' + JSON.stringify(notification));
    });

    // Method called when tapping on a notification
    PushNotifications.addListener('pushNotificationActionPerformed', (notification) => {
      console.log('Push action performed: ' + JSON.stringify(notification));
    });
  }
}

State Management

Angular Services

// services/data.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private usersSubject = new BehaviorSubject<User[]>([]);
  public users$: Observable<User[]> = this.usersSubject.asObservable();

  private loadingSubject = new BehaviorSubject<boolean>(false);
  public loading$: Observable<boolean> = this.loadingSubject.asObservable();

  constructor() {}

  getUsers(): User[] {
    return this.usersSubject.value;
  }

  setUsers(users: User[]): void {
    this.usersSubject.next(users);
  }

  addUser(user: User): void {
    const currentUsers = this.usersSubject.value;
    this.usersSubject.next([...currentUsers, user]);
  }

  updateUser(updatedUser: User): void {
    const currentUsers = this.usersSubject.value;
    const index = currentUsers.findIndex(user => user.id === updatedUser.id);
    if (index !== -1) {
      currentUsers[index] = updatedUser;
      this.usersSubject.next([...currentUsers]);
    }
  }

  deleteUser(id: number): void {
    const currentUsers = this.usersSubject.value;
    const filteredUsers = currentUsers.filter(user => user.id !== id);
    this.usersSubject.next(filteredUsers);
  }

  setLoading(loading: boolean): void {
    this.loadingSubject.next(loading);
  }
}

// Using the service in a component
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DataService, User } from '../services/data.service';

@Component({
  selector: 'app-users',
  templateUrl: './users.page.html',
  styleUrls: ['./users.page.scss'],
})
export class UsersPage implements OnInit, OnDestroy {
  users: User[] = [];
  loading = false;
  private subscriptions: Subscription[] = [];

  constructor(private dataService: DataService) {}

  ngOnInit() {
    // Subscribe to users
    this.subscriptions.push(
      this.dataService.users$.subscribe(users => {
        this.users = users;
      })
    );

    // Subscribe to loading state
    this.subscriptions.push(
      this.dataService.loading$.subscribe(loading => {
        this.loading = loading;
      })
    );
  }

  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
  }

  addUser() {
    const newUser: User = {
      id: Date.now(),
      name: 'New User',
      email: 'user@example.com'
    };
    this.dataService.addUser(newUser);
  }

  deleteUser(id: number) {
    this.dataService.deleteUser(id);
  }
}

NgRx (Advanced State Management)

# Install NgRx
npm install @ngrx/store @ngrx/effects @ngrx/store-devtools
// store/user.actions.ts
import { createAction, props } from '@ngrx/store';
import { User } from '../models/user.model';

export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
  '[User] Load Users Success',
  props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
  '[User] Load Users Failure',
  props<{ error: any }>()
);

export const addUser = createAction(
  '[User] Add User',
  props<{ user: User }>()
);

// store/user.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { User } from '../models/user.model';
import * as UserActions from './user.actions';

export interface UserState {
  users: User[];
  loading: boolean;
  error: any;
}

export const initialState: UserState = {
  users: [],
  loading: false,
  error: null
};

export const userReducer = createReducer(
  initialState,
  on(UserActions.loadUsers, state => ({
    ...state,
    loading: true
  })),
  on(UserActions.loadUsersSuccess, (state, { users }) => ({
    ...state,
    loading: false,
    users
  })),
  on(UserActions.loadUsersFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  })),
  on(UserActions.addUser, (state, { user }) => ({
    ...state,
    users: [...state.users, user]
  }))
);

// store/user.selectors.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
import { UserState } from './user.reducer';

export const selectUserState = createFeatureSelector<UserState>('users');

export const selectAllUsers = createSelector(
  selectUserState,
  (state: UserState) => state.users
);

export const selectUsersLoading = createSelector(
  selectUserState,
  (state: UserState) => state.loading
);

export const selectUsersError = createSelector(
  selectUserState,
  (state: UserState) => state.error
);

HTTP and Data

HTTP Client

// services/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, map } from 'rxjs/operators';

export interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
}

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private baseUrl = 'https://api.example.com';
  private httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    })
  };

  constructor(private http: HttpClient) {}

  // GET request
  get<T>(endpoint: string): Observable<T> {
    return this.http.get<T>(`${this.baseUrl}/${endpoint}`, this.httpOptions)
      .pipe(
        retry(3),
        catchError(this.handleError)
      );
  }

  // POST request
  post<T>(endpoint: string, data: any): Observable<T> {
    return this.http.post<T>(`${this.baseUrl}/${endpoint}`, data, this.httpOptions)
      .pipe(
        catchError(this.handleError)
      );
  }

  // PUT request
  put<T>(endpoint: string, data: any): Observable<T> {
    return this.http.put<T>(`${this.baseUrl}/${endpoint}`, data, this.httpOptions)
      .pipe(
        catchError(this.handleError)
      );
  }

  // DELETE request
  delete<T>(endpoint: string): Observable<T> {
    return this.http.delete<T>(`${this.baseUrl}/${endpoint}`, this.httpOptions)
      .pipe(
        catchError(this.handleError)
      );
  }

  // Error handling
  private handleError(error: HttpErrorResponse) {
    let errorMessage = 'Unknown error!';
    if (error.error instanceof ErrorEvent) {
      // Client-side errors
      errorMessage = `Error: ${error.error.message}`;
    } else {
      // Server-side errors
      errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
    }
    console.error(errorMessage);
    return throwError(errorMessage);
  }

  // Upload file
  uploadFile(endpoint: string, file: File): Observable<any> {
    const formData = new FormData();
    formData.append('file', file);

    return this.http.post(`${this.baseUrl}/${endpoint}`, formData)
      .pipe(
        catchError(this.handleError)
      );
  }
}

// Using the API service
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../services/api.service';
import { LoadingController, ToastController } from '@ionic/angular';

@Component({
  selector: 'app-data',
  templateUrl: './data.page.html',
  styleUrls: ['./data.page.scss'],
})
export class DataPage implements OnInit {
  data: any[] = [];

  constructor(
    private apiService: ApiService,
    private loadingController: LoadingController,
    private toastController: ToastController
  ) {}

  ngOnInit() {
    this.loadData();
  }

  async loadData() {
    const loading = await this.loadingController.create({
      message: 'Loading data...'
    });
    await loading.present();

    this.apiService.get('users').subscribe({
      next: (response: any) => {
        this.data = response.data || response;
        loading.dismiss();
      },
      error: async (error) => {
        loading.dismiss();
        const toast = await this.toastController.create({
          message: 'Error loading data: ' + error,
          duration: 3000,
          color: 'danger'
        });
        toast.present();
      }
    });
  }

  async saveData(item: any) {
    const loading = await this.loadingController.create({
      message: 'Saving...'
    });
    await loading.present();

    this.apiService.post('users', item).subscribe({
      next: async (response) => {
        loading.dismiss();
        const toast = await this.toastController.create({
          message: 'Data saved successfully!',
          duration: 2000,
          color: 'success'
        });
        toast.present();
        this.loadData(); // Refresh data
      },
      error: async (error) => {
        loading.dismiss();
        const toast = await this.toastController.create({
          message: 'Error saving data: ' + error,
          duration: 3000,
          color: 'danger'
        });
        toast.present();
      }
    });
  }
}

Storage

Ionic Storage

# Install Ionic Storage
npm install @ionic/storage-angular
// services/storage.service.ts
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';

@Injectable({
  providedIn: 'root'
})
export class StorageService {
  private _storage: Storage | null = null;

  constructor(private storage: Storage) {
    this.init();
  }

  async init() {
    const storage = await this.storage.create();
    this._storage = storage;
  }

  // Store data
  public async set(key: string, value: any): Promise<any> {
    return this._storage?.set(key, value);
  }

  // Get data
  public async get(key: string): Promise<any> {
    return this._storage?.get(key);
  }

  // Remove data
  public async remove(key: string): Promise<any> {
    return this._storage?.remove(key);
  }

  // Clear all data
  public async clear(): Promise<void> {
    return this._storage?.clear();
  }

  // Get all keys
  public async keys(): Promise<string[]> {
    return this._storage?.keys() || [];
  }

  // Get length
  public async length(): Promise<number> {
    return this._storage?.length() || 0;
  }

  // Store object
  public async setObject(key: string, object: any): Promise<any> {
    return this._storage?.set(key, JSON.stringify(object));
  }

  // Get object
  public async getObject(key: string): Promise<any> {
    const data = await this._storage?.get(key);
    return data ? JSON.parse(data) : null;
  }
}

// app.module.ts
import { IonicStorageModule } from '@ionic/storage-angular';

@NgModule({
  imports: [
    IonicStorageModule.forRoot()
  ]
})
export class AppModule {}

// Using storage service
import { Component, OnInit } from '@angular/core';
import { StorageService } from '../services/storage.service';

@Component({
  selector: 'app-settings',
  templateUrl: './settings.page.html',
  styleUrls: ['./settings.page.scss'],
})
export class SettingsPage implements OnInit {
  settings = {
    notifications: true,
    darkMode: false,
    language: 'en'
  };

  constructor(private storageService: StorageService) {}

  async ngOnInit() {
    await this.loadSettings();
  }

  async loadSettings() {
    const savedSettings = await this.storageService.getObject('app-settings');
    if (savedSettings) {
      this.settings = { ...this.settings, ...savedSettings };
    }
  }

  async saveSettings() {
    await this.storageService.setObject('app-settings', this.settings);
    console.log('Settings saved');
  }

  async resetSettings() {
    await this.storageService.remove('app-settings');
    this.settings = {
      notifications: true,
      darkMode: false,
      language: 'en'
    };
    console.log('Settings reset');
  }
}

Capacitor Preferences

// Install Capacitor Preferences
// npm install @capacitor/preferences

import { Preferences } from '@capacitor/preferences';

export class PreferencesService {
  // Set a value
  async setValue(key: string, value: string): Promise<void> {
    await Preferences.set({
      key: key,
      value: value,
    });
  }

  // Get a value
  async getValue(key: string): Promise<string | null> {
    const { value } = await Preferences.get({ key: key });
    return value;
  }

  // Remove a value
  async removeValue(key: string): Promise<void> {
    await Preferences.remove({ key: key });
  }

  // Clear all values
  async clearAll(): Promise<void> {
    await Preferences.clear();
  }

  // Get all keys
  async getAllKeys(): Promise<string[]> {
    const { keys } = await Preferences.keys();
    return keys;
  }

  // Store object
  async setObject(key: string, value: any): Promise<void> {
    await this.setValue(key, JSON.stringify(value));
  }

  // Get object
  async getObject(key: string): Promise<any> {
    const value = await this.getValue(key);
    return value ? JSON.parse(value) : null;
  }
}

Testing

Unit Testing

// home.page.spec.ts
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { HomePage } from './home.page';

describe('HomePage', () => {
  let component: HomePage;
  let fixture: ComponentFixture<HomePage>;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [HomePage],
      imports: [IonicModule.forRoot()]
    }).compileComponents();

    fixture = TestBed.createComponent(HomePage);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should have correct title', () => {
    expect(component.title).toBe('Welcome to Ionic');
  });

  it('should handle item click', () => {
    spyOn(console, 'log');
    const item = { id: 1, name: 'Test Item' };

    component.onItemClick(item);

    expect(console.log).toHaveBeenCalledWith('Item clicked:', item);
  });
});

// Service testing
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiService } from './api.service';

describe('ApiService', () => {
  let service: ApiService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [ApiService]
    });
    service = TestBed.inject(ApiService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should fetch users', () => {
    const mockUsers = [
      { id: 1, name: 'John Doe', email: 'john@example.com' },
      { id: 2, name: 'Jane Doe', email: 'jane@example.com' }
    ];

    service.get('users').subscribe(users => {
      expect(users).toEqual(mockUsers);
    });

    const req = httpMock.expectOne('https://api.example.com/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
});

E2E Testing

// e2e/src/app.e2e-spec.ts
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';

describe('new App', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getTitleText()).toContain('Welcome to Ionic');
  });

  it('should navigate to details page', () => {
    page.navigateTo();
    page.clickFirstItem();
    expect(browser.getCurrentUrl()).toContain('/details');
  });

  afterEach(async () => {
    // Assert that there are no errors emitted from the browser
    const logs = await browser.manage().logs().get(logging.Type.BROWSER);
    expect(logs).not.toContain(jasmine.objectContaining({
      level: logging.Level.SEVERE,
    } as logging.Entry));
  });
});

// e2e/src/app.po.ts
import { browser, by, element } from 'protractor';

export class AppPage {
  navigateTo(): Promise<unknown> {
    return browser.get(browser.baseUrl) as Promise<unknown>;
  }

  getTitleText(): Promise<string> {
    return element(by.css('app-home ion-title')).getText() as Promise<string>;
  }

  clickFirstItem(): Promise<void> {
    return element(by.css('app-home ion-item:first-child')).click() as Promise<void>;
  }
}

Building and Deployment

Build Commands

# Build for production
ionic build --prod

# Build with specific configuration
ionic build --configuration=staging

# Build for specific platform
ionic capacitor build android
ionic capacitor build ios

# Build and run
ionic capacitor run android
ionic capacitor run ios

# Build with live reload
ionic capacitor run android --livereload --external
ionic capacitor run ios --livereload --external

Android Deployment

# Add Android platform
ionic capacitor add android

# Sync changes
ionic capacitor sync android

# Open in Android Studio
ionic capacitor open android

# Build APK
cd android
./gradlew assembleDebug
./gradlew assembleRelease

# Build AAB (Android App Bundle)
./gradlew bundleRelease

# Install on device
adb install app/build/outputs/apk/debug/app-debug.apk

iOS Deployment

# Add iOS platform
ionic capacitor add ios

# Sync changes
ionic capacitor sync ios

# Open in Xcode
ionic capacitor open ios

# Build from command line
xcodebuild -workspace ios/App/App.xcworkspace -scheme App -configuration Release -destination generic/platform=iOS -archivePath ios/App/App.xcarchive archive

# Export IPA
xcodebuild -exportArchive -archivePath ios/App/App.xcarchive -exportPath ios/App/App.ipa -exportOptionsPlist ios/App/ExportOptions.plist

Web Deployment

# Build for web
ionic build --prod

# Deploy to Firebase Hosting
npm install -g firebase-tools
firebase login
firebase init hosting
firebase deploy

# Deploy to Netlify
npm install -g netlify-cli
netlify deploy --prod --dir=www

# Deploy to Vercel
npm install -g vercel
vercel --prod

Capacitor Integration

Capacitor Configuration

// capacitor.config.json
{
  "appId": "com.example.myapp",
  "appName": "My App",
  "webDir": "www",
  "bundledWebRuntime": false,
  "plugins": {
    "Camera": {
      "permissions": ["camera", "photos"]
    },
    "Geolocation": {
      "permissions": ["location"]
    },
    "PushNotifications": {
      "presentationOptions": ["badge", "sound", "alert"]
    }
  },
  "server": {
    "androidScheme": "https"
  }
}

Custom Capacitor Plugin

// Create a custom plugin
npm init @capacitor/plugin my-plugin

// src/definitions.ts
export interface MyPluginPlugin {
  echo(options: { value: string }): Promise<{ value: string }>;
  getDeviceId(): Promise<{ deviceId: string }>;
}

// src/web.ts
import { WebPlugin } from '@capacitor/core';
import type { MyPluginPlugin } from './definitions';

export class MyPluginWeb extends WebPlugin implements MyPluginPlugin {
  async echo(options: { value: string }): Promise<{ value: string }> {
    console.log('ECHO', options);
    return options;
  }

  async getDeviceId(): Promise<{ deviceId: string }> {
    return { deviceId: 'web-device-id' };
  }
}

// src/index.ts
import { registerPlugin } from '@capacitor/core';
import type { MyPluginPlugin } from './definitions';

const MyPlugin = registerPlugin<MyPluginPlugin>('MyPlugin', {
  web: () => import('./web').then(m => new m.MyPluginWeb()),
});

export * from './definitions';
export { MyPlugin };

// Using the plugin
import { MyPlugin } from 'my-plugin';

export class HomePage {
  async usePlugin() {
    const result = await MyPlugin.echo({ value: 'Hello World' });
    console.log(result.value);

    const deviceInfo = await MyPlugin.getDeviceId();
    console.log(deviceInfo.deviceId);
  }
}

Performance Optimization

Lazy Loading

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'home',
    loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
  },
  {
    path: 'profile',
    loadChildren: () => import('./profile/profile.module').then(m => m.ProfilePageModule)
  }
];

// Preloading strategy
@NgModule({
  imports: [
    RouterModule.forRoot(routes, { 
      preloadingStrategy: PreloadAllModules 
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Virtual Scrolling

<!-- For large lists -->
<ion-content>
  <ion-virtual-scroll [items]="items" approxItemHeight="70px">
    <ion-item *virtualItem="let item; let itemBounds = bounds;">
      <ion-avatar slot="start">
        <img [src]="item.avatar" />
      </ion-avatar>
      <ion-label>
        <h2>{{ item.name }}</h2>
        <p>{{ item.description }}</p>
      </ion-label>
    </ion-item>
  </ion-virtual-scroll>
</ion-content>

Image Optimization

// Lazy loading images
export class ImageOptimizationPage {
  images = [
    { src: 'assets/images/image1.jpg', loaded: false },
    { src: 'assets/images/image2.jpg', loaded: false },
    { src: 'assets/images/image3.jpg', loaded: false }
  ];

  onImageLoad(image: any) {
    image.loaded = true;
  }

  onImageError(image: any) {
    image.src = 'assets/images/placeholder.jpg';
    image.loaded = true;
  }
}
<!-- Lazy loading template -->
<ion-content>
  <div *ngFor="let image of images" class="image-container">
    <img 
      [src]="image.src" 
      [class.loaded]="image.loaded"
      (load)="onImageLoad(image)"
      (error)="onImageError(image)"
      loading="lazy"
    />
    <ion-spinner *ngIf="!image.loaded" name="crescent"></ion-spinner>
  </div>
</ion-content>

Bundle Analysis

# Analyze bundle size
npm install -g webpack-bundle-analyzer

# Build with stats
ionic build --prod --stats-json

# Analyze
npx webpack-bundle-analyzer www/stats.json

Best Practices

Code Organization

src/
├── app/
│   ├── core/              # Core functionality (guards, interceptors)
│   ├── shared/            # Shared components, pipes, directives
│   ├── features/          # Feature modules
│   │   ├── auth/          # Authentication feature
│   │   ├── profile/       # Profile feature
│   │   └── settings/      # Settings feature
│   ├── services/          # Global services
│   ├── models/            # Data models and interfaces
│   ├── utils/             # Utility functions
│   └── constants/         # App constants
├── assets/                # Static assets
├── environments/          # Environment configurations
└── theme/                 # Global styles and themes

Performance Guidelines

// Use OnPush change detection strategy
@Component({
  selector: 'app-optimized',
  templateUrl: './optimized.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
  @Input() data: any;

  constructor(private cdr: ChangeDetectorRef) {}

  updateData(newData: any) {
    this.data = newData;
    this.cdr.markForCheck();
  }
}

// Use trackBy functions for ngFor
trackByFn(index: number, item: any): any {
  return item.id || index;
}

// Unsubscribe from observables
export class ComponentWithSubscriptions implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    this.dataService.getData()
      .pipe(takeUntil(this.destroy$))
      .subscribe(data => {
        // Handle data
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

// Use async pipe when possible
@Component({
  template: `
    <div *ngFor="let item of items$ | async">
      {{ item.name }}
    </div>
  `
})
export class AsyncPipeComponent {
  items$ = this.dataService.getItems();

  constructor(private dataService: DataService) {}
}

Security Best Practices

// Sanitize user input
import { DomSanitizer } from '@angular/platform-browser';

export class SecurityComponent {
  constructor(private sanitizer: DomSanitizer) {}

  sanitizeHtml(html: string) {
    return this.sanitizer.sanitize(SecurityContext.HTML, html);
  }

  sanitizeUrl(url: string) {
    return this.sanitizer.sanitize(SecurityContext.URL, url);
  }
}

// Use environment variables for sensitive data
// environments/environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.production.com',
  apiKey: process.env['API_KEY'] // Use environment variables
};

// Implement proper error handling
export class ErrorHandlerService {
  handleError(error: any): void {
    // Log error to external service
    console.error('An error occurred:', error);

    // Don't expose sensitive information
    const userMessage = this.getUserFriendlyMessage(error);
    this.showToast(userMessage);
  }

  private getUserFriendlyMessage(error: any): string {
    if (error.status === 404) {
      return 'Resource not found';
    } else if (error.status === 500) {
      return 'Server error. Please try again later.';
    }
    return 'An unexpected error occurred';
  }
}

Troubleshooting

Common Issues and Solutions

Build Issues

# Clear cache and reinstall
rm -rf node_modules
rm package-lock.json
npm install

# Clear Ionic cache
ionic cache clear

# Reset Capacitor
npx cap clean
npx cap sync

Platform-Specific Issues

# Android issues
# Clean and rebuild
cd android
./gradlew clean
cd ..
ionic capacitor sync android

# iOS issues
# Clean derived data
rm -rf ~/Library/Developer/Xcode/DerivedData
cd ios/App
pod install
cd ../..
ionic capacitor sync ios

Runtime Errors

// Handle platform-specific code
import { Platform } from '@ionic/angular';

export class PlatformService {
  constructor(private platform: Platform) {}

  isNative(): boolean {
    return this.platform.is('capacitor');
  }

  isWeb(): boolean {
    return !this.platform.is('capacitor');
  }

  isIOS(): boolean {
    return this.platform.is('ios');
  }

  isAndroid(): boolean {
    return this.platform.is('android');
  }

  async runOnPlatform(callback: () => void): Promise<void> {
    await this.platform.ready();
    callback();
  }
}

// Error boundary component
@Component({
  selector: 'app-error-boundary',
  template: `
    <div *ngIf="hasError; else content" class="error-container">
      <ion-icon name="warning-outline"></ion-icon>
      <h2>Something went wrong</h2>
      <p>{{ errorMessage }}</p>
      <ion-button (click)="retry()">Try Again</ion-button>
    </div>
    <ng-template #content>
      <ng-content></ng-content>
    </ng-template>
  `
})
export class ErrorBoundaryComponent {
  hasError = false;
  errorMessage = '';

  @HostListener('window:error', ['$event'])
  handleError(event: ErrorEvent) {
    this.hasError = true;
    this.errorMessage = event.message || 'An unexpected error occurred';
  }

  retry() {
    this.hasError = false;
    this.errorMessage = '';
    window.location.reload();
  }
}

Summary

Ionic is a powerful framework for building cross-platform mobile applications using web technologies. Its key advantages include:

  • Cross-Platform Development: Write once, run on iOS, Android, and web
  • Web Technologies: Use familiar HTML, CSS, and JavaScript/TypeScript
  • Native Performance: Access native device features through Capacitor
  • Rich UI Components: Comprehensive library of mobile-optimized components
  • Framework Flexibility: Works with Angular, React, and Vue
  • Rapid Development: Fast development cycle with live reload
  • Strong Ecosystem: Extensive plugin ecosystem and community support
  • Cost-Effective: Reduce development time and maintenance costs

Ionic excels at enabling web developers to build native-quality mobile applications while leveraging existing web development skills and maintaining a single codebase across multiple platforms.