Ionic

Neil HaddleyDecember 22, 2021

Cross-platform apps.Powered by the Web.

Starting an app

$ npm install -g @ionic/cli

$ ionic start <name>

In this case we will create a todo Progressive Web Application (PWA)

$ ionic start haddley-todo

React

React

blank

blank

Running

$ cd <name>

$ ionic serve

ionic serve

ionic serve

blank

blank

Visual Studio Code

$ code .

Visual Studio Code

Visual Studio Code

IndexDB

Modern browsers support IndexDB.

The Localbase package makes it easier to work with IndexDB.

$ npm install localbase --save

npm install localbase

npm install localbase

localbase.d.ts

Localbases does not include typescript type definitions.

To workaround this issue create a localbase.d.ts file

localbase.d.ts

localbase.d.ts

contents

contents

todos.ts

Create a TypeScript file that defines the "Todo" interface and uses Localbase to fetch, create and update todo items.

todos.ts

todos.ts

TodoListItem

A react component to display each todo item.

Created using Ionic Web Components {IonItem, IonCheckbox and IonLabel}

TodoListItem

TodoListItem

Home.tsx

The home page fetches todo items from browser's IndexDB using the getTodos() method defined in todos.ts.

The home page uses the setComplete(...) method to toggle the completed value.

The home page uses the addToDo(...) method to add a new todo item.

The home page uses Ionic Web Components to display a text box and a list of TodoListItems {IonButton, IonContent, IonHeader, IonInput, IonItem, IonList, IonPage, IonRefresher, IonRefresherContent, IonTitle and IonToolbar}.

The IonRefresher Web Component allows a user to refresh the list by swiping the list from top to bottom.

Home.tsx

Home.tsx

Web Inspector (Safari)

Web Inspector (Safari)

pwa-asset-generator

The pwa-asset-generator can be used to generate a set of application icons from a single .jpg image

pwa-asset-generator

pwa-asset-generator

index.html

Update public/index.html to include the links

generated icons

generated icons

manifest.json

The public/manifest.json file provides details of the web application.

Update the manifest.json file to reference the generated icons

default manifest.json

default manifest.json

updated manifest.json

Update the application name.

Updated manifest.json

Updated manifest.json

Service Worker

Service Worker allows the PWA to run offline.

Service Worker allows the PWA to upgrade while online.

serviceWorkerRegistration.register(...)

serviceWorkerRegistration.register(...)

build

$ ionic build

ionic build

ionic build

Deploy to Azure

Deploy to Static Website via Azure Storage...

Deploy to Static Website...

Deploy to Static Website...

Create new Storage Account...

Create new Storage Account...

haddleytodo

haddleytodo

Creating...

Creating...

Deployment complete

Deployment complete

Running in safari (from Azure)

Running in safari (from Azure)

Running on iPhone Simulator (Safari)

Running on iPhone Simulator (Safari)

Add to Home Screen

Add to Home Screen

Add

Add

Icon on home screen

Icon on home screen

Running (online)

Running (online)

Updated version available

Updated version available

IonItemSliding

Add the IonItemSliding tag and the user is able to swipe an item from right to left to reveal a Delete option.

IonItemSliding

IonItemSliding

Swipe list item right to left

Swipe list item right to left

PouchDB

PouchDB is an open-source JavaScript database inspired by Apache CouchDB that is designed to run well within the browser.

PouchDB was created to help web developers build applications that work as well offline as they do online.

CouchDB

BASH
1% docker run -p 5984:5984 -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -d couchdb
2
3% curl localhost:5984

http://localhost:5984/_utils/

The code below demonstrates how the todo application was updated to use PouchDB and CouchDB.

docker run ...

docker run ...

curl localhost:5984

curl localhost:5984

_utils

_utils

Enable CORS

Enable CORS

No databases

No databases

Adding Task 1 using Safari

Adding Task 1 using Safari

Task 1 has been added

Task 1 has been added

Task 1 details have been replicated to the CouchDB server

Task 1 details have been replicated to the CouchDB server

All documents

All documents

Task 1 details

Task 1 details

Task 1 has been replicated to Chrome

Task 1 has been replicated to Chrome

Task 1 has been updated using Chrome

Task 1 has been updated using Chrome

Task 1 update has been replicated to Safari

Task 1 update has been replicated to Safari

CouchDB is stopped

CouchDB is stopped

Multiple changes made offline using Safari and Chrome

Multiple changes made offline using Safari and Chrome

CouchDB server started

CouchDB server started

Multiple updates are replicated to/from Safari/Chrome

Multiple updates are replicated to/from Safari/Chrome

datatodos.ts

TEXT
1import Localbase from 'localbase'
2
3function createGuid() {
4    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
5        var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
6        return v.toString(16);
7    });
8}
9
10export interface Todo {
11    guid: string;
12    task: string;
13    completed: boolean;
14}
15
16let db = new Localbase('todos')
17
18export const getTodos = async () => {
19    const todos = await db.collection('tasks').get()
20    console.log(todos)
21    return todos
22}
23
24export const setCompleted: any = async (guid: string, completed: boolean) => {
25    await db.collection('tasks').doc({ guid: guid }).update({
26        completed: completed
27    })
28    return
29}
30
31export const addToDo: any = async (description: string) => {
32    const guid = createGuid();
33    await db.collection('tasks').add({
34        guid: guid,
35        task: description,
36        completed: false
37    })
38    return guid
39}

componentsTodoListItem.tsx

TEXT
1import {
2  IonCheckbox,
3  IonItem,
4  IonLabel,
5  } from '@ionic/react';
6import { Todo } from '../data/todos';
7import './TodoListItem.css';
8
9interface TodoListItemProps {
10  todo: Todo;
11  toggleTodo: any;
12}
13
14const TodoListItem: React.FC<TodoListItemProps> = ({ todo, toggleTodo }) => {
15  return (
16    <IonItem >
17      <IonCheckbox checked={todo.completed} onIonChange={e => toggleTodo(todo.guid)} slot="start"/>
18      <IonLabel className="ion-text-wrap">
19          {todo.task}
20      </IonLabel>
21    </IonItem>
22  );
23};
24
25export default TodoListItem;

pagesHome.tsx

TEXT
1import { IonButton, IonContent, IonHeader, IonInput, IonItem, IonList, IonPage, IonRefresher, IonRefresherContent, IonTitle, IonToolbar, useIonViewWillEnter } from '@ionic/react';
2import { useState } from 'react';
3import TodoListItem from '../components/TodoListItem';
4import { addToDo, getTodos, setCompleted, Todo } from '../data/todos';
5import './Home.css';
6
7const Home: React.FC = () => {
8
9  const [todos, setTodos] = useState<Todo[]>([]);
10  const [text, setText] = useState<string>();
11
12  useIonViewWillEnter(async () => {
13    const todos = await getTodos();
14    setTodos(todos);
15  });
16
17
18  const toggleTodo = async (guid: string) => {
19    const existing = todos.find((todo) => todo.guid === guid)
20    if (existing) {
21      const completed = !existing.completed
22      await setCompleted(guid, completed)
23      // refresh from db
24      const todos = await getTodos();
25      setTodos(todos);
26    }
27  }
28
29  const addTask = async () => {
30    if (text) {
31      const guid = await addToDo(text)
32      console.log(guid)
33      //reset textbox
34      setText(() => '')
35      // refresh from db
36      const todos = await getTodos();
37      setTodos(todos);
38    }
39  }
40
41  const refresh = async (e: CustomEvent) => {
42    // refresh from db
43    const todos = await getTodos();
44    setTodos(todos);
45    e.detail.complete();
46  };
47
48  const HandleKeyPress = (value: any) => {
49    if (value == "Enter") {
50      //  console.log("Enter key pressed")
51      addTask()
52    }
53  }
54
55  return (
56    <IonPage>
57      <IonHeader>
58        <IonToolbar>
59          <IonTitle>Tasks</IonTitle>
60        </IonToolbar>
61      </IonHeader>
62      <IonContent fullscreen>
63
64        <IonRefresher slot="fixed" onIonRefresh={refresh}>
65          <IonRefresherContent></IonRefresherContent>
66        </IonRefresher>
67
68        <IonItem>
69          <IonInput value={text} placeholder="Enter task" onKeyPress={e => HandleKeyPress(e.key)} onIonChange={e => setText(e.detail.value!)}></IonInput>
70          <IonButton slot="end" onClick={() => addTask()}>Add</IonButton>
71        </IonItem>
72
73        <IonList>
74          {todos.map(m => <TodoListItem key={m.guid} todo={m} toggleTodo={toggleTodo} />)}
75        </IonList>
76
77
78      </IonContent>
79    </IonPage>
80  );
81};
82
83export default Home;

publicmanifest.json

TEXT
1{
2  "short_name": "Todo",
3  "name": "Todo App",
4  "icons": [
5    {
6      "src": "manifest-icon-192.maskable.png",
7      "sizes": "192x192",
8      "type": "image/png",
9      "purpose": "any"
10    },
11    {
12      "src": "manifest-icon-192.maskable.png",
13      "sizes": "192x192",
14      "type": "image/png",
15      "purpose": "maskable"
16    },
17    {
18      "src": "manifest-icon-512.maskable.png",
19      "sizes": "512x512",
20      "type": "image/png",
21      "purpose": "any"
22    },
23    {
24      "src": "manifest-icon-512.maskable.png",
25      "sizes": "512x512",
26      "type": "image/png",
27      "purpose": "maskable"
28    }
29  ],
30  "start_url": ".",
31  "display": "standalone",
32  "theme_color": "#ffffff",
33  "background_color": "#ffffff"
34}

index.tsx

TEXT
1import React from 'react';
2import ReactDOM from 'react-dom';
3import App from './App';
4import * as serviceWorkerRegistration from './serviceWorkerRegistration';
5import reportWebVitals from './reportWebVitals';
6
7ReactDOM.render(
8  <React.StrictMode>
9    <App />
10  </React.StrictMode>,
11  document.getElementById('root')
12);
13
14// If you want your app to work offline and load faster, you can change
15// unregister() to register() below. Note this comes with some pitfalls.
16// Learn more about service workers: https://cra.link/PWA
17// serviceWorkerRegistration.unregister();
18
19const config = {
20  onUpdate: (registration: any) => { alert("An updated version of this app is available.  Please close and re-start the app.") },
21}
22
23serviceWorkerRegistration.register(config);
24
25// If you want to start measuring performance in your app, pass a function
26// to log results (for example: reportWebVitals(console.log))
27// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
28reportWebVitals();

todos.ts

TEXT
1import PouchDB from 'pouchdb-browser'
2
3const db = new PouchDB('tasksdb')
4const remotedburl = 'http://admin:password@localhost:5984/tasksdb'
5
6function createGuid() {
7    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
8        var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
9        return v.toString(16);
10    });
11}
12
13export interface Todo {
14    guid: string;
15    task: string;
16    completed: boolean;
17    _id: string,
18    _rev: string
19}
20
21export const getTodos = async () => {
22    await replicateFrom()
23    const todos = await db.allDocs({
24        include_docs: true,
25        attachments: false
26    })
27    return todos.rows.map((row: { doc: any; }) => <Todo>row.doc)
28}
29
30export const setCompleted = async (existing: Todo, completed: boolean) => {
31    try {
32        await db.put({
33            ...existing,
34            completed
35        })
36        await replicateTo()
37    }
38    catch (err) {
39        console.log("err", err)
40    }
41}
42
43export const deleteTodo = async (existing: Todo) => {
44    try {
45        await db.remove(
46            existing._id,
47            existing._rev
48        )
49        await replicateTo()
50    }
51    catch (err) {
52        console.log("err", err)
53    }
54}
55
56export const addToDo = async (description: string) => {
57    try {
58        const guid = createGuid();
59        await db.post({
60            guid: guid,
61            task: description,
62            completed: false
63        })
64        await replicateTo()
65    }
66    catch (err) {
67        console.log("err", err)
68    }
69}
70
71export const replicate = async () => {
72    await replicateFrom()
73    await replicateTo()
74}
75
76const replicateFrom = async () => {
77    try {
78        const result = await db.replicate.from(remotedburl)
79        console.log("result", result)
80    }
81    catch (err) {
82        console.log("err", err)
83    }
84}
85
86const replicateTo = async () => {
87    try {
88        const result = await db.replicate.to(remotedburl)
89        console.log("result", result)
90    }
91    catch (err) {
92        console.log("err", err)
93    }
94}

References