Ionic
Neil Haddley • December 22, 2021
Cross-platform apps.Powered by the Web.
Starting an app
BASH
1$ npm install -g @ionic/cli 2$ ionic start <name>
I created a todo Progressive Web Application (PWA)
BASH
1$ ionic start haddley-todo
Running
BASH
1$ cd <name> 2$ ionic serve
Visual Studio Code
BASH
1$ code .
IndexDB
Modern browsers support IndexDB.
The Localbase package makes it easier to work with IndexDB.
BASH
1$ npm install localbase --save
localbase.d.ts
Localbases does not include typescript type definitions.
To workaround this issue create a localbase.d.ts file
todos.ts
I created a TypeScript file that defines the "Todo" interface and uses Localbase to fetch, create and update todo items.
TodoListItem
I created a React component to display each todo item.
Created using Ionic Web Components {IonItem, IonCheckbox and IonLabel}
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.
pwa-asset-generator
The pwa-asset-generator can be used to generate a set of application icons from a single .jpg image
index.html
I updated public/index.html to include the links
manifest.json
The public/manifest.json file provides details of the web application.
I updated the manifest.json file to reference the generated icons
updated manifest.json
I updated the application name.
Service Worker
Service Worker allows the PWA to run offline.
Service Worker allows the PWA to upgrade while online.
build
BASH
1$ ionic build
Deploy to Azure
I deployed to Static Website via Azure Storage.
IonItemSliding
I added the IonItemSliding tag so the user could swipe an item from right to left to reveal a Delete option.
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 shows how I updated the todo application to use PouchDB and CouchDB.
datatodos.ts
TYPESCRIPT
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
TYPESCRIPT
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
TYPESCRIPT
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
JSON
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
TYPESCRIPT
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
TYPESCRIPT
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}
















































