DEV Community

Cover image for Ionic React JS AWS Amplify Authentication CRUD Tutorial Using Datastore & Storage API
Aaron K Saunders
Aaron K Saunders

Posted on

Ionic React JS AWS Amplify Authentication CRUD Tutorial Using Datastore & Storage API

What We Are Building

mobile application image

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; 
Enter fullscreen mode Exit fullscreen mode
// 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; 
Enter fullscreen mode Exit fullscreen mode
// 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; 
Enter fullscreen mode Exit fullscreen mode
// 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; }; 
Enter fullscreen mode Exit fullscreen mode

Important Links

Top comments (0)