What We Are Building
Videos
- Ionic React JS AWS Amplify Authentication CRUD Tutorial Part 1, Authentication UI Component: Integrate AWS Amplify with Ionic React JS - this is part one of a series of video about using AWS Amplify with React JS and Ionic Framework. In the series, we will utilize the new Amplify Admin UI, Create Models and some relationships and then finally deploy the application to Amazon for hosting.
- Ionic React JS AWS Amplify Authentication CRUD Tutorial Part 2, Authentication UI Component: Integrate AWS Amplify with Ionic React JS - this is part one of a series of video about using AWS Amplify with React JS and Ionic Framework. We will utilize the new Amplify Admin UI, Create Models and some relationships and then finally deploy the application to Amazon for hosting.
- Ionic React JS AWS Amplify Authentication CRUD Tutorial Part 3, Working With Storage API and Images: We cover a lot in this video of using AWS Amplify Storage and Datastore APIs with Ionic Framework and React JS.
We are associating an image with each Task object. The image is stored in an S3Bucket using the AWS Amplify Storage API. We also show one approach for changing the image associated with the Task object or removing the image completely from the Task Object.
Final Source Code
// src/App.tsx
import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import Home from './pages/Home';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';
/* Theme variables */
import './theme/variables.css';
const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/home">
<Home />
</Route>
<Route exact path="/">
<Redirect to="/home" />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
export default App;
// src/pages/Home.tsx
import {
IonButton,
IonButtons,
IonContent,
IonFooter,
IonHeader,
IonModal,
IonPage,
IonTitle,
IonToolbar,
} from "@ionic/react";
import "./Home.css";
import Amplify, { DataStore, Storage } from "aws-amplify";
import { AmplifyAuthenticator, AmplifySignOut } from "@aws-amplify/ui-react";
import awsconfig from "../aws-exports";
import React, { useState } from "react";
import ListTasks from "../components/ListTasks";
import EntryForm from "../components/EntryForm";
import { Task } from "../models";
import { create } from "domain";
Amplify.configure(awsconfig);
const Home: React.FC = () => {
const [showModal, setShowModal] = useState({ isOpen: false, data: null });
/**
*
* @param value
* @returns
*/
const updateTask = async (value: any) => {
// update
const original = await DataStore.query(Task, value.id);
if (original === undefined) return;
// check to see if there is a new file
let fileSaveResp: any = null;
// save original imageKey
let newImageKey: any = original.imageKey;
if (value?.file?.lastModified) {
// saving new image
fileSaveResp = await Storage.put(
encodeURIComponent(value?.file.name),
value?.file
);
// delete old image
if (original.imageKey) await Storage.remove(original.imageKey);
// set new imageKey
newImageKey = fileSaveResp?.key;
}
// User Cleared Out The EXISTING Image
else if (original.imageKey && !value?.file) {
// delete the old image, dont add anything
if (original.imageKey) await Storage.remove(original.imageKey);
// clear the imageKey since there is no file
newImageKey = null;
}
return await DataStore.save(
Task.copyOf(original, (updated: any) => {
updated.title = value.title;
updated.description = value.description;
updated.imageKey = newImageKey
})
);
};
/**
*
* @param value
*/
const createTask = async (value: any) => {
// create
let fileResp: any = null;
if (value?.file) {
fileResp = await Storage.put(
encodeURIComponent(value?.file.name),
value?.file
);
}
return await DataStore.save(
new Task({
title: value?.title,
description: value?.description,
imageKey: fileResp?.key,
})
);
};
/**
*
* @param value
*/
const handleCloseModal = async (value: any) => {
console.log(value);
if (value) {
if (value?.id) {
await updateTask(value);
} else {
await createTask(value);
}
}
setShowModal({ isOpen: false, data: null });
};
/**
*
* @param value
*/
const editData = (value: any) => {
setShowModal({ isOpen: true, data: value });
};
/**
*
* @param value
*/
const deleteData = async (value: any) => {
await DataStore.delete(Task, (t: any) => t.id("eq", value.id));
};
return (
<AmplifyAuthenticator usernameAlias="email">
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Ionic React AWS Amplify</IonTitle>
<IonButtons slot="end">
<IonButton
onClick={() => setShowModal({ isOpen: true, data: null })}
>
NEW
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
{/* <!-- MODAL --> */}
<IonModal isOpen={showModal.isOpen}>
<EntryForm
onCloseModal={handleCloseModal}
initialValues={showModal.data}
/>
</IonModal>
<ListTasks onDelete={deleteData} onEdit={editData} />
</IonContent>
<IonFooter>
<AmplifySignOut />
</IonFooter>
</IonPage>
</AmplifyAuthenticator>
);
};
export default Home;
// src/components/EntryForm.tsx
import React, { useEffect, useRef, useState } from "react";
import {
IonButton,
IonButtons,
IonContent,
IonFooter,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonInput,
IonItem,
IonLabel,
IonTextarea,
} from "@ionic/react";
export interface EntryFormProps {
onCloseModal: any;
initialValues: any;
}
const EntryForm: React.FC<EntryFormProps> = ({
onCloseModal,
initialValues,
}) => {
const titleRef = useRef<any>("");
const descriptionRef = useRef<any>("");
const fileRef = useRef<any>(null);
const [currentFile, setCurrentFile] = useState<any>(null);
useEffect(() => {
titleRef.current.value = initialValues?.title;
descriptionRef.current.value = initialValues?.description;
if (initialValues?.imageKey) {
const name = decodeURIComponent(initialValues?.imageKey);
setCurrentFile({ name });
}
}, [initialValues]);
/**
*
* @param e
*/
const handleSave = (e: any) => {
e.preventDefault();
onCloseModal({
title: titleRef.current?.value,
description: descriptionRef.current?.value,
file: currentFile,
id: initialValues?.id || null,
});
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>ENTRY FORM</IonTitle>
<IonButtons slot="end"></IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonItem>
<IonLabel>Title</IonLabel>
<IonInput ref={titleRef as any}></IonInput>
</IonItem>
<IonItem>
<IonLabel>Description</IonLabel>
<IonTextarea ref={descriptionRef as any} rows={3}></IonTextarea>
</IonItem>
{/* <!-- get file --> */}
<IonItem>
<IonLabel>
<p>{currentFile ? currentFile?.name : null}</p>
</IonLabel>
<input
ref={fileRef as any}
type="file"
style={{ display: "none" }}
onChange={(e) =>
setCurrentFile(e?.target?.files ? e?.target?.files[0] : null)
}
/>
{currentFile ? (
<IonButton onClick={() => setCurrentFile(null)} color="danger">
Clear
</IonButton>
) : (
<IonButton onClick={() => fileRef.current.click()}>
Select File
</IonButton>
)}
</IonItem>
</IonContent>
<IonFooter>
<IonToolbar>
<IonButton type="button" onClick={() => onCloseModal(null)}>
CANCEL
</IonButton>
<IonButton onClick={handleSave}>SAVE</IonButton>
</IonToolbar>
</IonFooter>
</IonPage>
);
};
export default EntryForm;
// src/components/ListTasks.tsx
import {
IonButton,
IonLabel,
IonList,
IonCard,
IonCardContent,
} from "@ionic/react";
import { DataStore, Storage } from "aws-amplify";
import React, { useEffect, useState } from "react";
import { Task } from "../models";
export interface ListTasksProps {
onEdit: any;
onDelete: any;
}
const ListTasks: React.FC<ListTasksProps> = ({ onDelete, onEdit }) => {
const [data, setData] = useState([] as any);
useEffect(() => {
const loadData = async () => {
const tasks = await DataStore.query(Task);
setData(tasks);
};
loadData();
// data changes
const resp = DataStore.observe(Task).subscribe(() => {
loadData();
});
return () => resp.unsubscribe();
}, []);
return (
<IonList>
{data.map((t: any) => (
<IonCard key={t.id}>
<IonCardContent>
<div style={{ width: "auto" }}>
<ImageRender imageKey={t?.imageKey}></ImageRender>
</div>
<IonLabel class="ion-text-wrap">
<p>{t.title}</p>
<p>{t.description}</p>
<p style={{ zoom: 0.8 }}>{t.id}</p>
<div
className="ion-float-right ion-padding"
style={{ paddingRight: "0" }}
>
<IonButton style={{ zoom: 0.8 }} onClick={() => onEdit(t)}>
EDIT
</IonButton>
<IonButton
style={{ zoom: 0.8 }}
onClick={() => onDelete(t)}
color="danger"
>
DELETE
</IonButton>
</div>
</IonLabel>
</IonCardContent>
</IonCard>
))}
</IonList>
);
};
export default ListTasks;
export interface ImageRenderProps {
imageKey: string;
}
const ImageRender: React.FC<ImageRenderProps> = ({ imageKey }) => {
const [imageURL, setImageURL] = useState<any>("");
useEffect(() => {
try {
imageKey &&
Storage.get(imageKey, { download: true }).then((result: any) => {
setImageURL(URL.createObjectURL(result.Body));
});
} catch (e) {
setImageURL(null);
}
}, [imageKey]);
return imageKey && imageURL ? <img src={imageURL} alt={imageKey} /> : null;
};
Important Links
- Ionic React - https://ionicframework.com/docs/react
- Amplify Setup - https://docs.amplify.aws/cli/start/install
- Amplify React - https://docs.amplify.aws/ui/q/framework/react
- Amplify React UI - https://docs.amplify.aws/ui/auth/authenticator/q/framework/react
- Amplify Datastore API - https://docs.amplify.aws/lib/datastore/getting-started/q/platform/js
- Amplify Storage API - https://docs.amplify.aws/lib/storage/getting-started/q/platform/js