DEV Community

Cover image for TO-DO List - CRUD Full Stack in Ionic Type Script React and Parse Back4app
Omar Zeinhom
Omar Zeinhom

Posted on

TO-DO List - CRUD Full Stack in Ionic Type Script React and Parse Back4app

About

Demo

An ionic 6 , web application built in typescript and react js framework ,while in terms of the backend back 4app was used as the api .

- Start By


yarn install

  • ADD Api keys from parse dashboard back4app
    1. Select your application or create a new one
  • 1.1 - Make ACLS public . Note this is not recommended for deployment only development

    1. Go to App settings on the left
    1. Select security and keys and get the api keys
REACT_APP_PARSE_ID=
REACT_APP_PARSE_HOST_URL=
REACT_APP_PARSE_JS_KEY=
Enter fullscreen mode Exit fullscreen mode
  • After these simple steps Serve application and Enjoy !

Start By 🚀

ionic serve
Enter fullscreen mode Exit fullscreen mode
  • Project Built With

Project Requirements

  1. nodejs 16.18.0

Make sure you installed node and node package manager using

npm -v

and

node -v

  1. yarn
  • Install yarn by using

npm install -g yarn

Enter fullscreen mode Exit fullscreen mode
  1. ionic framework

npm i -g @ionic/cli

Enter fullscreen mode Exit fullscreen mode

Setup the project

ionic start todoApp --type=react --capacitor
Enter fullscreen mode Exit fullscreen mode

-- use yarn instead of npm

ionic config set -g yarn true

Packages to install

parse From parse - yarn pkg

@parse/react

From @parse/react - yarn pkg

& Getting started with the Parse React hook for real time updates using Parse

yarn add @parse/react parse
Enter fullscreen mode Exit fullscreen mode
Project Structure and files to add
- public
- /assets               // images
-  /icons               // favicon.ico for example
- index.html            // the html rendered webpage
- src                   //root folder
- /components           // where all the components reside
-  /CreateToDo          //1. create new folder inside ./src/components/ call it CreateToDo
-  /CreateToDo.tsx      //2.  create new file inside /src/components/CreateToDo call it CreateToDo.tsx 
- /pages                //where all pages reside
-  /EditToDo            //3.create new folder inside ./src/pages/ call it EditToDo
-  /EditToDo.tsx        //4. create new file inside ./src/pages/EditToDo call it EditToDo.tsx 
- /theme                // Where ionic app.css styles reside
-   /variables.css      // ionic default css variables for dark or light mode
- App.tsx               // Where the application component resides, the ionic router and also initializeParse Client
- index.tsx             // Where the application renders in the index.html <div id="root" ></div>  
- .env                  // Where all the Api Keys are going to be saftely stored for production

Enter fullscreen mode Exit fullscreen mode
  1. CREATE [x]
// ADD IMPORTS 
import React, { useState, useEffect } from "react";
import {
  IonCol,
  IonLabel,
  IonInput,
  IonTextarea,
  IonButton,
  IonIcon,
  IonGrid,
  IonRow,
  IonItem,
  IonText,
} from "@ionic/react";

import { add,paperPlaneOutline } from "ionicons/icons";

const Parse = require("parse");

// Export a default function 

export default function CreateToDo() {

    return (
        <></>
    )
    }
Enter fullscreen mode Exit fullscreen mode
 //ADD STATE VAR AND STATE ACTION AND ASSIGN PROPERTIES
  const [newToDoObject, setNewToDoObject] = useState({
    title: "",
    description: "",
    task: "",
    isCompleted: false,
    createdAt: new Date(),
    updatedAt: new Date(),
  });
Enter fullscreen mode Exit fullscreen mode
//ADD async arrow function to handle creating the new to object{}

 const createNewToDoObject = async () => {
    const newToDo = new Parse.Object("ToDo", newToDoObject);
    newToDo.set(newToDoObject);

    try {
      const newToDoObject = await newToDo.save();

      const newToDoObjJSON = JSON.stringify(newToDoObject);

      alert("The New To Do Object Has been Created >>>>! " + newToDoObjJSON);
    } catch (error: any) {
      alert("Errro was found in createNewToDoObject " + error);
    }
  };
Enter fullscreen mode Exit fullscreen mode

//Hanlde ToDoChg 

const handleToDoCHG= (event: any)=> {

setNewToDoObject((previous : any)=> ({
  ...previous,
  [event.target.name]: event.target.value,
}));

//html5

};
Enter fullscreen mode Exit fullscreen mode
  • Make sure to match the html5 property name with the properties passed to the object
  • Also add onIonChange={handleToDoCHG} in each input to handle the users input
//ADD html5 name property and handleToDoCHG to handle the user inputs change 
<IonGrid fixed={true}>
  <IonText>
  Create ToDo <IonIcon icon={paperPlaneOutline}/>
  </IonText>

    <IonInput name="title" onIonChange={handleToDoCHG} placeholder="Enter Title here..." maxlength={25}/>

      <IonInput name="task" onIonChange={handleToDoCHG} placeholder="Enter Task here..." maxlength={25} />


      <IonTextarea name="description" onIonChange={handleToDoCHG} style={{resize: "none"}} placeholder="Enter Description here..."  maxlength={100}/>


<IonButton onClick={createNewToDoObject} expand="block" color={"success"}> <IonIcon icon={add} />
</IonButton>
</IonGrid>

Enter fullscreen mode Exit fullscreen mode

Final File CreateToDo.tsx

//CreateToDo.tsx
import React, { useState, useEffect } from "react";
import {
  IonCol,
  IonLabel,
  IonInput,
  IonTextarea,
  IonButton,
  IonIcon,
  IonGrid,
  IonRow,
  IonItem,
  IonText,
} from "@ionic/react";

import { add, paperPlaneOutline } from "ionicons/icons";

const Parse = require("parse");

export default function CreateToDo() {
  //STATE VAR AND STATE ACTION AND ASSIGN PROPERTIES
  const [newToDoObject, setNewToDoObject] = useState({
    title: "",
    description: "",
    task: "",
    isCompleted: false,
    createdAt: new Date(),
    updatedAt: new Date(),
  });

  const createNewToDoObject = async () => {
    const newToDo = new Parse.Object("ToDo", newToDoObject);
    newToDo.set(newToDoObject);

    try {
      const newToDoObject = await newToDo.save();

      const newToDoObjJSON = JSON.stringify(newToDoObject);

      alert("The New To Do Object Has been Created >>>>! " + newToDoObjJSON);
    } catch (error: any) {
      alert("Errro was found in createNewToDoObject " + error);
    }
  };

  //Hanlde ToDoChg

  const handleToDoCHG = (event: any) => {
    setNewToDoObject((previous: any) => ({
      ...previous,
      [event.target.name]: event.target.value,
    }));

    //html5
  };

  return (
    <>
      <IonGrid fixed={true}>
        <IonText>
          Create ToDo <IonIcon icon={paperPlaneOutline} />
        </IonText>
        <IonRow>
          <IonCol size="6">
            <IonItem>
              <IonLabel color={"success"} position="stacked">
                Title
              </IonLabel>
              <IonInput
                name="title"
                onIonChange={handleToDoCHG}
                placeholder="Enter Title here..."
                maxlength={25}
              />
            </IonItem>
          </IonCol>

          <IonCol size="6">
            <IonItem>
              <IonLabel color={"success"} position="stacked">
                Task
              </IonLabel>
              <IonInput
                name="task"
                onIonChange={handleToDoCHG}
                placeholder="Enter Task here..."
                maxlength={25}
              />
            </IonItem>
          </IonCol>

          <IonCol size="10">
            <IonItem>
              <IonLabel color={"success"} position="stacked">
                Description
              </IonLabel>
              <IonTextarea
                name="description"
                onIonChange={handleToDoCHG}
                style={{ resize: "none" }}
                placeholder="Enter Description here..."
                maxlength={100}
              />
            </IonItem>
          </IonCol>

          <IonCol size="2">
            <IonButton
              onClick={createNewToDoObject}
              expand="block"
              color={"success"}
            >
              {" "}
              <IonIcon icon={add} />
            </IonButton>
          </IonCol>
        </IonRow>
      </IonGrid>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
  1. READ [x]
  • For this part you can assign a new component in ./src/component/EditToDo/EdiToDo.tsx
//2-A. SET STATE VAR And SetStateAction
  var [toDos, setToDos] = useState([
    {
      objectId: " ",
      title: "",
      description: "",
      task: "",
      isCompleted: Boolean(),
      createdAt: new Date(),
      updatedAt: new Date(),
    },
  ]);

Enter fullscreen mode Exit fullscreen mode
 //2-B. extending the Parse object
  const ToDo: Parse.Object[] = Parse.Object.extend("ToDo"); // extend todo

  const parsequery: Parse.Query = new Parse.Query(ToDo);

Enter fullscreen mode Exit fullscreen mode
  //2-C. ASYNC Function to handle reading tasks with useCallback hook to handle each task instead of going in an infinte loop
  const readTasks = useCallback(async function (): Promise<Boolean> {
    try {
      const results: Parse.Object[] = await parsequery.find();

      const mappedData = [];

      for (const object of results) {
        const objId: string = object.id;
        const title: string = object.get("title");
        const decription: string = object.get("description");
        const task: string = object.get("task");
        const isCompleted: boolean = object.get("isCompleted");
        const createdAt: Date = object.get("createdAt");
        const updatedAt: Date = object.get("updatedAt");

        let resultsFix = {
          objectId: objId, //string
          title: title, //string
          description: decription,
          task: task,
          isCompleted: isCompleted, //boolean
          createdAt: createdAt, //date
          updatedAt: updatedAt, //date
        };

        mappedData.push(resultsFix);
      }
      setToDos(mappedData);
      return true;
    } catch (error: any) {
      console.warn("Error has been found in readTasks " + error);
      return false;
    }
  }, []);

  console.log(toDos);
Enter fullscreen mode Exit fullscreen mode
  // 2-D. useEffect
  useEffect(() => {
    readTasks();
    //uncomment these lines after addint the refreshTasks async arrow function
    //refreshTasks();
  }, [readTasks, /*refreshTasks*/]);
Enter fullscreen mode Exit fullscreen mode
  1. UPDATE [X]
    //UPDATE TODO

        const completeTask = async () => {
          try {
            const object = await parsequery.get(objId);
            object.set("isCompleted", true);
            object.set("objectId", objId);
            object.save();
          } catch (error: any) {
            console.warn("Error has been found in completeTask" + error);
          }
        };

Enter fullscreen mode Exit fullscreen mode
  1. DELETE [X]
   //DELETE TODO
        const deleteToDo = async () => {
          try {
            const singleObject: Parse.Object = await parsequery.get(objId);

            const response: any = await singleObject.destroy();

            if (response) {
              alert(`${objId} To Do Has Been Deleted`);
            } else {
              alert(`Error: Nothing was Delted`);
            }

            return true;
          } catch (error: any) {
            console.warn("Error has been found in deleteToDo" + error);
          }
        };

Enter fullscreen mode Exit fullscreen mode
  1. Refresh Tasks
 /*-------------< TODO REFRESH TASKS START >---------*/
  const refreshTasks = useCallback(
    async function () {
      var query = new Parse.Query("ToDo");
      query
        .find()
        .then((results: Parse.Object) => {
          //DEBUG
          //Stringified Value of Results
          //const resultsStr = JSON.stringify(results);
          //console.log("Results of ToDo parse Object is >>>" + resultsStr);
          //
        })
        .then(() => {
          query.count().then((ToDoCount: Number) => {
            console.log("Number of tasks is = " + ToDoCount);
          });
        })
        .catch((error: any) => {
          // error is an instance of parse.error.
          console.log(error);
        });
      //REFRESH TASKS TO REMOVE THE DELETED ONES ID
      readTasks();
      return true;
    },
    [readTasks]
  );
  /*-------------< TODO REFRESH TASKS END >---------*/

Enter fullscreen mode Exit fullscreen mode

Final File in ./src/components/EditToDo/EditToDo.tsx

import React from "react";
import {
  IonButton,
  IonCard,
  IonCardContent,
  IonCardHeader,
  IonCardSubtitle,
  IonCol,
  IonIcon,
  IonItem,
  IonText,
  IonCheckbox,
  IonBadge,
  IonRippleEffect,
  IonRow,
  IonGrid,
} from "@ionic/react";

import { close, returnDownBack } from "ionicons/icons";

import { FC, ReactElement, useCallback, useEffect, useState } from "react";

const Parse = require("parse");

const EditToDo: FC<{}> = (): ReactElement => {
  //1. STATE VAR And SetStateAction
  var [toDos, setToDos] = useState([
    {
      objectId: " ",
      title: "",
      description: "",
      task: "",
      isCompleted: Boolean(),
      createdAt: new Date(),
      updatedAt: new Date(),
    },
  ]);

  // extending the Parse object
  const ToDo: Parse.Object[] = Parse.Object.extend("ToDo"); // extend todo

  const parsequery: Parse.Query = new Parse.Query(ToDo);

  //2. ASYNC Function to handle reading tasks with useCallback hook to handle each task instead of going in an infinte loop
  const readTasks = useCallback(async function (): Promise<Boolean> {
    try {
      const results: Parse.Object[] = await parsequery.find();

      const mappedData = [];

      for (const object of results) {
        const objId: string = object.id;
        const title: string = object.get("title");
        const decription: string = object.get("description");
        const task: string = object.get("task");
        const isCompleted: boolean = object.get("isCompleted");
        const createdAt: Date = object.get("createdAt");
        const updatedAt: Date = object.get("updatedAt");

        let resultsFix = {
          objectId: objId, //string
          title: title, //string
          description: decription,
          task: task,
          isCompleted: isCompleted, //boolean
          createdAt: createdAt, //date
          updatedAt: updatedAt, //date
        };

        mappedData.push(resultsFix);
      }
      setToDos(mappedData);
      return true;
    } catch (error: any) {
      console.warn("Error has been found in readTasks " + error);
      return false;
    }
  }, []);

  console.log(toDos);

  /*-------------< TODO REFRESH TASKS START >---------*/
  const refreshTasks = useCallback(
    async function () {
      var query = new Parse.Query("ToDo");
      query
        .find()
        .then((results: Parse.Object) => {
          //DEBUG
          //Stringified Value of Results
          //const resultsStr = JSON.stringify(results);
          //console.log("Results of ToDo parse Object is >>>" + resultsStr);
          //
        })
        .then(() => {
          query.count().then((ToDoCount: Number) => {
            console.log("Number of tasks is = " + ToDoCount);
          });
        })
        .catch((error: any) => {
          // error is an instance of parse.error.
          console.log(error);
        });
      //REFRESH TASKS TO REMOVE THE DELETED ONES ID
      readTasks();
      return true;
    },
    [readTasks]
  );
  /*-------------< TODO REFRESH TASKS END >---------*/

  // 3. useEffect
  useEffect(() => {
    readTasks();
    refreshTasks();
  }, [readTasks, refreshTasks]);

  return (
    <>
      <IonRow>
        <IonCol size="10">
          <IonButton onClick={refreshTasks} color="secondary" expand="block">
            <IonIcon icon={returnDownBack} />
          </IonButton>
        </IonCol>

        <IonCol size="2">
          <IonBadge color={"medium"}>{toDos?.length}</IonBadge>
        </IonCol>
      </IonRow>

      {toDos?.map((todo: any, index: any) => {
        // MAP OVER THE TODOS AND RETURN THE INFO

        //GET ID

        var objId: string = todo?.objectId;
        //console.log(objId);

        //DELETE TODO
        const deleteToDo = async () => {
          try {
            const singleObject: Parse.Object = await parsequery.get(objId);

            const response: any = await singleObject.destroy();

            if (response) {
              alert(`${objId} To Do Has Been Deleted`);
            } else {
              alert(`Error: Nothing was Delted`);
            }

            return true;
          } catch (error: any) {
            console.warn("Error has been found in deleteToDo" + error);
          }
        };

        //UPDATE TODO

        const completeTask = async () => {
          try {
            const object = await parsequery.get(objId);
            object.set("isCompleted", true);
            object.set("objectId", objId);
            object.save();
          } catch (error: any) {
            console.warn("Error has been found in completeTask" + error);
          }
        };

        return (
          <div key={todo + index}>
            <IonGrid fixed={true}>
              <IonRippleEffect></IonRippleEffect>

              <IonCard color={todo.isCompleted === true ? "success" : "medium"}>
                <IonCardHeader
                  color={todo?.isCompleted === true ? "light" : "warning"}
                >
                  <IonRow>
                    <IonCol size="9">
                      <IonText
                        color={todo?.isCompleted === true ? "dark" : "light"}
                      >
                        <h5>{[todo?.title?.toLocaleUpperCase() || " "]}</h5>
                      </IonText>
                    </IonCol>
                    <IonCol size="3">
                      <IonButton
                        color="danger"
                        expand="block"
                        onClick={deleteToDo}
                      >
                        <IonIcon icon={close} />{" "}
                      </IonButton>
                    </IonCol>
                  </IonRow>
                </IonCardHeader>

                <IonItem
                  color={todo?.isCompleted === true ? "success" : "medium"}
                >
                  <IonText color={"light"}>
                    Task :{[todo?.task?.toLocaleLowerCase() || " "]}
                  </IonText>
                </IonItem>

                <IonCardSubtitle className="ion-text-center">
                  <h5 className="ion-text-white">
                    <strong>Description</strong>
                  </h5>
                  <em>{[todo?.description?.toLocaleLowerCase() || " "]}</em>
                </IonCardSubtitle>

                <IonCardContent>
                  <IonRow>
                    <IonCol size="10">
                      <table>
                        <thead>
                          <tr>
                            <th>Task</th>
                            <th>Completed</th>
                            <th>CreatedAt</th>
                            <th>updatedAt</th>
                          </tr>
                        </thead>

                        <tbody>
                          <tr>
                            <td> {todo?.task}</td>
                            <td>
                              {" "}
                              <IonCheckbox
                                color="medium"
                                // eslint-disable-next-line react/jsx-no-duplicate-props
                                onClick={completeTask}
                                disabled={todo?.isCompleted === true}
                              />{" "}
                              {todo?.isCompleted.toLocaleString()}
                            </td>
                            <td> {todo.createdAt?.toDateString()}</td>
                            <td> {todo.updatedAt?.toDateString()}</td>
                          </tr>
                        </tbody>
                      </table>
                    </IonCol>
                  </IonRow>
                </IonCardContent>
              </IonCard>
            </IonGrid>
          </div>
        );
      })}
    </>
  );
};

export default EditToDo;


Enter fullscreen mode Exit fullscreen mode
References
  1. Signing up in Parser - Back4App Docs
  2. Logging Page in Parser - Back4App Docs
  3. How TO - Responsive Text W3Schools
  4. User Password Reset for React Parse - Back4App Docs
  5. Theming Basics Ionic-framework Colors
  6. Theming Basics Ionic-framework Colors customziation
  7. aaronksaunders-ionic-react-tabs-side-auth
  8. Stringify a JavaScript Array
  9. GitHubMapBoxLanguage
  10. Map-Box Ar Example
  11. CodePen HomeChange a map's language
  12. Parse~ ParseQuery
  13. use-react-memo-wisely/
  14. React.memo
  15. Migrating from npm
  16. Colors - Ionic
  17. Parse JS Guide
  18. Building Your Own Hooks
  19. react-chat-app - Back4App Docs
  20. React CRUD tutorial - Back4App Docs
  21. Ionic - Inputs
  22. Ionic - IonCheckBox
  23. Ionic - ion-radio
  24. this operator js - MDN Docs
  25. Ionic -ion-grid
  26. Does not provide a valid apple-touch-icon
  27. Ionic -React Navigation
  28. ReactJs - useCallback hook
  29. Using Yarn Instead of Npm for Ionic #10647

2.

Omar Zeinhom . AKA ANDGOEDU 2022-2023

Top comments (0)