DEV Community

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

Posted on

6

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

Do your career a big favor. Join DEV. (The website you're on right now)

It takes one minute, it's free, and is worth it for your career.

Get started

Community matters

Top comments (0)

Sentry image

See why 4M developers consider Sentry, β€œnot bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

πŸ‘‹ Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay