--- title: "Configure the client" url: https://docs.catalyst.zoho.com/en/tutorials/todo-list/python/configure-client/ description: "Build a Basic, React, or Angular to-do list web application using Catalyst Advanced I/O Function and Catalyst Data Store that enables you to note down tasks and delete them after they are done." last_updated: 2026-08-17T12:16:04Z service: All Services type: tutorial related_links: - name: "Client Directory Structure" url: /en/cli/v1/project-directory-structure/client-directory --- # Configure the Client Next, let's configure the client component. Based on your earlier preference, you can navigate to the relevant section: **Basic web app** #### Configure a Basic web app The [client directory](https://docs.catalyst.zoho.com/en/cli/v1/project-directory-structure/client-directory) contains: * The `index.html` file that contains the HTML code for the front-end application. * The `main.css` file that contains the CSS code for the front-end application. * The `main.js` file that contains the JavaScript code. * The `client-package.json` configuration file We will be coding `index.html`, `main.css`, and `main.js`. > **Note**: Please go through the code given in this section to make sure you fully understand it. Copy the code snippets given below and paste it in the respective files located in the `client/` directory using an IDE, then save the files. **index.html** ```xml <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="A Simple Catalyst Application." /> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap" rel="stylesheet" /> <link rel="stylesheet" href="main.css" /> <script src="main.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script> <title>To Do</title> </head> <body> <div class="container"> <div class="dF aI-center jC-center h-inh" id="page-loader"> <div class="loader--lg"></div> </div> <div id="layout" class="dN"> <div class="title-container px-20"> <p class="text-white text-28 font-700">To Do</p> </div> <div class="create-container"> <form class="dF aI-center w-full" onsubmit="createTodo(event)" autocomplete="off"> <input id="notes" type="text" placeholder="Enter a Task" class="input input--valid" oninput="onNotesChange(this)" /> <button id="create-task-btn" class="btn btn--primary ml-10" type="submit"> Create Task <div class="btn--primary__loader ml-5 dN" id="create-task-btn-loader"></div> </button> </form> </div> <div class="task-container" id="tasks"> </div> </div> </div> </body> </html> ``` **main.css** ```css body, p { margin: 0; } * { box-sizing: border-box; font-family: 'Poppins', sans-serif; } input { margin: 0; resize: none; border: none; outline: none; -webkit-appearance: none; -moz-appearance: none; appearance: none; } button { padding: 0; border: none; outline: none; cursor: pointer; background: transparent; } /* Application Styles \*/ .container { height: 100vh; } .title-container { top: 0; left: 0; right: 0; z-index: 1; height: 4rem; display: flex; position: fixed; align-items: center; background: rgb(5, 150, 105); } .create-container { z-index: 1; top: 4rem; left: 0; right: 0; height: 5rem; padding: 20px; display: flex; position: fixed; background: #fff; align-items: center; } .task-container { padding-top: 9rem; } .task { display: flex; margin: 0 20px; font-size: 16px; padding: 12px 20px; align-items: center; border-radius: 5px; } .task:hover { background: rgba(5, 150, 105, 0.1); } .task__no { color: #111111; margin-right: 5px; } .task__title { flex: 1; margin-right: 5px; word-break: break-all; } .task__btn { width: 18px; height: 18px; opacity: 0.5; } .task__btn:hover { opacity: 1; } .input { width: 100%; font-size: 15px; padding: 12px 16px; border-radius: 5px; border: 1px solid #e0e0e0; } .input:focus { border: 1px solid rgb(5, 150, 105); box-shadow: 0px 0px 1px 1px rgb(5, 150, 105); } .input::-moz-placeholder { color: #919191; font-size: 15px; } .input:-ms-input-placeholder { color: #919191; font-size: 15px; } .input::placeholder { color: #919191; font-size: 15px; } .input:disabled { background: #f8f8f8; } .loader--lg { width: 60px; height: 60px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 7px solid rgb(5, 150, 105); border-right: 7px solid rgb(5, 150, 105); border-bottom: 7px solid transparent; animation: spin 0.8s linear infinite; } .loader--sm { width: 25px; height: 25px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 4px solid rgb(5, 150, 105); border-right: 4px solid rgb(5, 150, 105); border-bottom: 4px solid transparent; -webkit-animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite; } .loader--xs { width: 20px; height: 20px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 4px solid rgb(5, 150, 105); border-right: 4px solid rgb(5, 150, 105); border-bottom: 4px solid transparent; -webkit-animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite; } .btn { flex-shrink: 0; display: flex; cursor: pointer; font-size: 15px; font-weight: 500; padding: 12px 16px; align-items: center; border-radius: 5px; } .btn--primary { color: #fff; background: rgb(5, 150, 105); } .btn--primary__loader { width: 15px; height: 15px; margin: 0 5px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 3px solid #fff; border-right: 3px solid #fff; border-bottom: 3px solid transparent; -webkit-animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite; } .btn--primary:disabled { cursor: not-allowed; background: rgba(5, 150, 105, 0.7); } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* Helper Styles */ .my-5 { margin-top: 5px; margin-bottom: 5px; } .mr-5 { margin-right: 5px; } .ml-10 { margin-left: 10px; } .px-20 { padding-left: 20px; padding-right: 20px; } .p-20 { padding: 20px; } .w-full { width: 100%; } .text-28 { font-size: 28px; } .text-16 { font-size: 16px; } .text-white { color: #fff; } .text-info { color: #919191; } .dF { display: flex; } .aI-center { align-items: center; } .jC-center { justify-content: center; } .flex-1 { flex-grow: 1; } .h-inh { height: inherit; } .font-700 { font-weight: 700; } .dN { display: none; } .dB { display: block; } ``` **main.js** ```javascript var page = 1; var todoItems = []; var hasMore = false; window.onload = () => { getTodos(1); toggleCreateTaskBtn(''); }; //GET API. Contains the logic to fetch existing to-do items. function getTodos() { $.ajax({ url: `/server/to_do_list_function/all?page=${page}&perPage=200`, //Ensure that 'to_do_list_function' is the package name of your function. success: function (data) { const { data: { todoItems, hasMore } } = data; window.todoItems = [ ...new Map( Array.from(window.todoItems) .concat(todoItems) .map((item) => [item.id, item]) ).values() ]; window.hasMore = hasMore; renderTodo(); }, error: function (err) { console.log(err); }, complete: function () { $('#infinite-scroll-loader').removeClass('dB').addClass('dN'); $('#page-loader').removeClass('dF').addClass('dN'); $('#layout').removeClass('dN').addClass('dB'); } }); } function onMouseEnter(element) { const id = element.id; const delBtn = $(`#${id}-del`); if (delBtn && delBtn.attr('data-deleting')) { delBtn.removeClass('dN').addClass('dB'); } } function onMouseLeave(element) { const id = element.id; const delBtn = $(`#${id}-del`); if (delBtn && delBtn.attr('data-deleting')) { delBtn.removeClass('dB').addClass('dN'); } } function onNotesChange(element) { toggleCreateTaskBtn($(`#${element.id}`).val()); } function toggleCreateTaskBtn(value) { if (value) { $('#create-task-btn').attr('disabled', false); } else { $('#create-task-btn').attr('disabled', true); } } // DELETE API. Contains the logic to delete a to-do item. function deleteTodo(id) { $(`#${id}-del`).attr({ disabled: true, 'data-deleting': true, class: 'dN' }); $(`#${id}-del-loader`).removeClass('dN').addClass('dB'); $.ajax({ method: 'DELETE', url: `/server/to_do_list_function/${id}`, success: function () { todoItems = todoItems.filter((obj) => obj.id !== id); renderTodo(); }, error: function (err) { console.log(err); $(`#${id}-del`).attr({ disabled: false, 'data-deleting': false, class: 'dB' }); $(`#${id}-del-loader`).removeClass('dB').addClass('dN'); } }); } function renderTodo() { if (!todoItems.length) { $('#tasks').html( ` <div class='p-20 dF jC-center'> <p class='text-info text-16'> No tasks available, Create a new task. </p> </div> ` ); } else { let html = ''; todoItems.forEach((item, index) => { html += `<div class='task' id=${item.id} onmouseenter="onMouseEnter(this)" onMouseLeave="onMouseLeave(this)" > <p class='task__no'>${index + 1 + ') '}</p> <p class='task__title'>${item.notes}</p> <div class='loader--xs dN' id="${item.id}-del-loader" ></div> <button id="${item.id + '-del'}" class="dN" onclick="deleteTodo('${item.id }')" data-deleting=false > <svg class='task__btn' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' > <path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16' ></path> </svg> </button> </div>`; }); $('#tasks').html(html); const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore) { page += 1; $('#tasks') .append(` <div class="dF jC-center my-5" id="infinite-scroll-loader"> <div class="loader--sm"></div> </div> `); getTodos(); } }); observer.observe( document.getElementById(todoItems[todoItems.length - 1].id) ); } } //POST API. Contains the logic to create a to-do item. function createTodo(event) { event.preventDefault(); const notes = $(`#notes`).val(); $(`#notes`).attr({ readOnly: true }); $('#create-task-btn').attr('disabled', true); $('#create-task-btn-loader').removeClass('dN').addClass('dB'); $.ajax({ method: 'POST', contentType: 'application/json', url: '/server/to_do_list_function/add', //Ensure that 'to_do_list_function' is the package name of your function. data: JSON.stringify({ notes }), success: function (data) { const { data: { todoItem } } = data; todoItems = [todoItem].concat(todoItems); renderTodo(); }, error: function (error) { console.log(error); }, complete: function () { $(`#notes`) .attr({ readOnly: false }) .val(''); $('#create-task-btn-loader').removeClass('dB').addClass('dN'); } }); return false; } ``` > **Note**: The Python function package name `to_do_list_function` has been added in the code for `main.js` in the lines **13** and **182**. If you have provided a different package name, ensure you replace accordingly. The client directory is now configured. Let's quickly go over the working of the function and the client code: 1. **POST Operation** * When the user enters a to-do list item in the app and saves it, the `submit` event associated with the **Create Task** button triggers an Ajax call to the POST API. * The `main.js` in the client handles the Ajax operation and the URL, and it calls the POST API defined in the `main.py` function file. * The POST API defined in `main.py` inserts the data as a record in the _TodoItems_ table in the Data Store. The list item is inserted as the value of the _Notes_ column. * After a record insertion is done, the response (new task) will be added to the to-do list. 2. **GET Operation** * The `reload` event triggers an Ajax call to the GET API. The URL and the Ajax operation are handled in `main.js`. * The GET API defined in `main.py` obtains all the records from the Data Store by executing a ZCQL query. The ZCQL query contains the fetch operation along with the start-limit and the end-limit of the number of records that can be fetched. * The response containing the records (tasks) will be added to the existing to-do list. 3. **DELETE Operation** * The `main.js` handles the Ajax call to the DELETE API. When the user hovers on a particular task, and clicks on the appearing delete-bin icon in the client app, the DELETE API is triggered. * The DELETE API defined in `main.py` then executes the delete operation for the record in the _TodoItems_ table matching the ROWID and sends the response back to the client. * The `main.js` removes the respective record (deleted task) from the to-do list and displays the updated to-do list in the client app upon a successful delete operation. **Angular web app** #### Configure an Angular web app The Angular web client directory contains the following files: * The root directory of the client contains a [`client-package.json`](https://docs.catalyst.zoho.com/en/cli/v1/project-directory-structure/client-directory)file, which is a configuration file defining the name, version, and default home page of the client component. * [Native Angular files](https://angular.io/docs) such as **`angular.json`, `karma.conf.js`, `tsconfig.app.json`, `tsconfig.json`, `tsconfig.spec.json` files, and the `dist`** directory. * The root directory of the client also contains the [`package.json`](https://docs.npmjs.com/files/package.json) dependency file, and a [`.gitignorefile`](https://git-scm.com/docs/gitignore). * The `src` folder contains the following files and directories as per the default project structure of the Angular app: * Native Angular files such as **`favicon.ico`, `main.ts`, `polyfills.ts`, and `test.ts`** files along with the assets, and environment directories. * `index.html`: The default entry point of the to-do list application. * `styles.css`: Contains all the style elements of the to-do list application. * The files present in the `src/app/` directory are: * Native Angular files such as **`app.component.css` and `app.component.spec.ts`**. * `app.component.html`: Contains the HTML component of each event generated by the user. * `app.component.ts`: Contains the logic of the to-do list application. * `app.module.ts`: Contains the meta of all the components used in building the to-do list application. We will be coding the `index.html`, `styles.css`, `app.component.html`, `app.component.ts`, and the `app.module.ts` files. #### Create a Task Component You must create a `task` component that contains the logic associated with each to-do task the end- user enters. Execute the following command in the **`client/src/app/`** directory to create the `task` component: ```sh ng generate component task ``` This will create a folder named `task` that contains: * The `task.css` and the `task.spec.ts` files native to Angular. * `task.html`: Contains the HTML component of each to-do task entered by the user. * `task.ts`: Contains the logic of each to-do task entered by the user. We will be coding the `task.html` and `task.ts` files as well. #### Install the Axios Package We will also need the `axios` package to create a request from client to server. To install `axios`, navigate to the client directory (`client/`) and execute the following command: ```sh npm install axios ``` This will install the axios module and save the dependencies. ![catalyst_todo_angular_axios](https://catalyst-docs-development.zohostratus.in/images/tutorials/todo-list/catalyst_todo_angular_axios.webp) You can now begin adding code in your files. > **Note**: Please go through the code given in this section to ensure you fully understand it. Copy the code given below and paste it in `index.html` and `styles.css` files located in client directory (`client/src/`) respectively using an IDE and save the file. **index.html** ```xml <!DOCTYPE html> <html lang="en"> <head> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap" rel="stylesheet" /> <meta charset="utf-8" /> <title>To Do</title> <base href='/app/'/> </head> <body> <app-root></app-root> </body> </html> ``` **styles.css** ```css body, p { margin: 0; } * { box-sizing: border-box; font-family: "Poppins", sans-serif; } input { margin: 0; resize: none; border: none; outline: none; -webkit-appearance: none; -moz-appearance: none; appearance: none; } button { padding: 0; border: none; outline: none; cursor: pointer; background: transparent; } /* Main Classess \*/ .container { height: 100vh; } .title-container { top: 0; left: 0; right: 0; z-index: 1; height: 4rem; display: flex; position: fixed; align-items: center; background: rgb(5, 150, 105); } .create-container { z-index: 1; top: 4rem; left: 0; right: 0; height: 5rem; padding: 20px; display: flex; position: fixed; background: #fff; align-items: center; } .task-container { padding-top: 9rem; } .task { display: flex; margin: 0 20px; font-size: 16px; padding: 12px 20px; align-items: center; border-radius: 5px; } .task:hover { background: rgba(5, 150, 105, 0.1); } .task__no { color: #111111; margin-right: 5px; } .task__title { flex: 1; margin-right: 5px; word-break: break-all; } .task__btn { width: 18px; height: 18px; opacity: 0.5; } .task__btn:hover { opacity: 1; } .input { width: 100%; font-size: 15px; padding: 12px 16px; border-radius: 5px; border: 1px solid #e0e0e0; } .input:focus { border: 1px solid rgb(5, 150, 105); box-shadow: 0px 0px 1px 1px rgb(5, 150, 105); } .input::-moz-placeholder { color: #919191; font-size: 15px; } .input:-ms-input-placeholder { color: #919191; font-size: 15px; } .input::placeholder { color: #919191; font-size: 15px; } .input:disabled { background: #f8f8f8; } .loader--lg { width: 60px; height: 60px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 7px solid rgb(5, 150, 105); border-right: 7px solid rgb(5, 150, 105); border-bottom: 7px solid transparent; animation: spin 0.8s linear infinite; } .loader--sm { width: 25px; height: 25px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 4px solid rgb(5, 150, 105); border-right: 4px solid rgb(5, 150, 105); border-bottom: 4px solid transparent; -webkit-animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite; } .loader--xs { width: 20px; height: 20px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 4px solid rgb(5, 150, 105); border-right: 4px solid rgb(5, 150, 105); border-bottom: 4px solid transparent; -webkit-animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite; } .btn { flex-shrink: 0; display: flex; cursor: pointer; font-size: 15px; font-weight: 500; padding: 12px 16px; align-items: center; border-radius: 5px; } .btn--primary { color: #fff; background: rgb(5, 150, 105); } .btn--primary__loader { width: 15px; height: 15px; margin: 0 5px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 3px solid #fff; border-right: 3px solid #fff; border-bottom: 3px solid transparent; -webkit-animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite; } .btn--primary:disabled { background: rgba(5, 150, 105, 0.7); } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* Helper Classes */ .my-5 { margin-top: 5px; margin-bottom: 5px; } .mr-5 { margin-right: 5px; } .ml-10 { margin-left: 10px; } .px-20 { padding-left: 20px; padding-right: 20px; } .p-20 { padding: 20px; } .w-full { width: 100%; } .text-28 { font-size: 28px; } .text-16 { font-size: 16px; } .text-white { color: #fff; } .text-info { color: #919191; } .dF { display: flex; } .aI-center { align-items: center; } .jC-center { justify-content: center; } .flex-1 { flex-grow: 1; } .h-inh { height: inherit; } .font-700 { font-weight: 700; } ``` Copy the code given below and paste it in `app.component.html`, `app.component.ts`, `app.module.ts` files respectively located in the client directory (`client/src/app/`) using an IDE and save the file. **app.component.html** ```xml <div class="container"> <div class="dF aI-center jC-center h-inh" *ngIf="fetchState === 'init'"> <div class="loader--lg"></div> </div> <div *ngIf="fetchState !== 'init'"> <div class="title-container px-20"> <p class="text-white text-28 font-700">To Do</p> </div> <div class="create-container"> <form class="dF aI-center w-full" (ngSubmit)="createTodo()" autocomplete="off" > <input type="text" name="notes" [(ngModel)]="notes" placeholder="Enter a Task" class="input input--valid" [readOnly]="submitting" /> <button class="btn btn--primary ml-10" type="submit" [disabled]="notes.length === 0 || submitting" > Create Task <div class="btn--primary__loader ml-5" *ngIf="submitting"></div> </button> </form> </div> <div class="task-container"> <div class="p-20 dF jC-center" *ngIf="todoItems.length === 0"> <p class="text-info text-16">No tasks available, Create a new task.</p> </div> <div *ngIf="todoItems.length !== 0"> <app-task *ngFor="let item of todoItems; let i = index" [notes]="item.notes" [index]="i + 1" [id]="item.id" [isLast]="i === todoItems.length - 1" (removeTodo)="removeTodo($event)" (changePage) = "changePage()" #task > </app-task> </div> <div class="dF jC-center my-5" *ngIf="fetchState === 'loading'"> <div class="loader--sm"></div> </div> </div> </div> </div> ``` **app.component.ts** ```javascript import axios from 'axios'; import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit { notes: string; page: number; hasMore: boolean; todoItems: Array<{ id: string; notes: string; }>; submitting: boolean; fetchState: 'init' | 'fetched' | 'loading'; constructor() { this.notes = ''; this.hasMore = false; this.page = 1; this.todoItems = []; this.submitting = false; this.fetchState = 'init'; } //GET API. The existing to-do items from the Datastore is being fetched. getTodos = (): void => { axios .get('/server/to_do_list_function/all', { //Ensure that 'to_do_list_function' is the package name of your function. params: { page: this.page, perPage: 200, }, }) .then((response) => { const { data: { todoItems, hasMore }, } = response.data; if (this.page === 1) { this.todoItems = todoItems as Array<{ id: string; notes: string }>; } else { this.todoItems = [ ...new Map( this.todoItems.concat(todoItems).map((item) => [item.id, item]) ).values(), ]; } this.hasMore = hasMore; this.fetchState = 'fetched'; }) .catch((err) => { console.error(err.response.data); }); }; //POST API. A new to-do item is being created. createTodo = (): void => { this.submitting = true; axios .post(`/server/to_do_list_function/add`, { //Ensure that 'to_do_list_function' is the package name of your function. notes: this.notes, }) .then((response) => { const { data: { todoItem }, } = response.data; this.notes = ''; this.todoItems = [{ ...todoItem }].concat(this.todoItems); }) .catch((err) => { console.error(err.response.data); }) .finally(() => { this.submitting = false; }); }; removeTodo = (id: string): void => { this.todoItems = this.todoItems.filter((obj) => obj.id !== id); }; changePage = (): void => { if (this.hasMore) { this.page += 1; this.fetchState = 'loading'; this.getTodos(); } }; ngOnInit() { this.fetchState = 'init'; this.getTodos(); } } ``` > **Note**: The Python function package's name `to_do_list_function` has been added in the code for `app.component.ts` in the lines **33**, and **65**. If you have provided a different package name, ensure you replace accordingly. **app.module.ts** ```javascript import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { Task } from './task/task'; @NgModule({ declarations: [AppComponent, Task], imports: [BrowserModule, FormsModule], providers: [], bootstrap: [AppComponent], }) export class AppModule {} ``` Copy the code given below and paste it in `task.html` and `task.ts`, files located in the client directory (`client/src/app/task/`) using an IDE and save the file. **task.html** ```xml <div class="task" ref="{ref}" (mouseenter)="mouseEnter()" (mouseleave)="mouseLeave()" > <p class="task__no">{{ index }} )</p> <p class="task__title">{{ notes }}</p> <div class="loader--xs" *ngIf="deleting === true"></div> <button *ngIf="!deleting && options" (click)="deleteTodo()"> <svg class="task__btn" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" > <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" ></path> </svg> </button> </div> ``` **task.ts** ```javascript import axios from 'axios'; import { Component, EventEmitter, Input, Output, ElementRef, AfterViewInit, OnDestroy, } from '@angular/core'; @Component({ selector: 'app-task', templateUrl: './task.html', }) export class Task implements OnDestroy, AfterViewInit { @Input() id: string; @Input() notes: string; @Input() index: number; @Input() isLast: boolean; @Output() removeTodo: EventEmitter<string>;; @Output() changePage: EventEmitter<void>; public options: boolean; public deleting: boolean; private observer?: IntersectionObserver; constructor(private element: ElementRef) { this.id = ''; this.index = 0; this.notes = ''; this.isLast = false; this.options = false; this.deleting = false; this.removeTodo = new EventEmitter(); this.changePage = new EventEmitter(); } mouseEnter = () => { this.options = true; }; mouseLeave = () => { this.options = false; }; deleteTodo = () => { this.deleting = true; //DELETE API. The call to delete the to-do item from the data store occurs here. axios .delete(`/server/to_do_list_function/${this.id}`) //Ensure that 'to_do_list_function' is the package name of your function. .then((response) => { const { data: { todoItem: { id }, }, } = response.data; this.removeTodo.emit(id); }) .catch((err) => { console.log(err.response.data); }) .finally(() => { this.deleting = false; }); }; ngAfterViewInit() { if (this.isLast && this.element) { this.observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { this.changePage.emit(); } }); this.observer.observe(this.element.nativeElement); } } ngOnDestroy() { if (this.observer) { this.observer.disconnect(); } } } ``` > **Note**: The Python function package's name `to_do_list_function` has been added in the code for `task.ts` in the lines **51**. If you have provided a different package name, ensure you replace accordingly. The client directory is now configured. Let's quickly go over the working of the function and the client code: 1. **POST Operation** * When the user enters a to-do list item in the app and saves it, the `submit` event **Create Task** button triggers an Ajax call to the POST API. * The `app.component.ts` in the client handles the Ajax operation and the URL, and it calls the POST API defined in the `main.py` function file. * The POST API defined in `main.py` inserts the data as a record in the _TodoItems_ table in the Data Store. The list item is inserted as the value of the _Notes_ column. * After a record insertion is done, the response (new task) will be added to the to-do list. 2. **GET Operation** * The `reload` event triggers an Ajax call to the GET API. The URL and the Ajax operation are handled in `app.component.ts`. * The GET API defined in `main.py` obtains all the records from the Data Store by executing a ZCQL query. The ZCQL query contains the fetch operation along with the start-limit and the end-limit of the number of records that can be fetched. * The response containing the records (tasks) will be added to the existing to-do list. 3. **DELETE Operation** * When the user clicks on a list item in the client app, the DELETE API is triggered. * The `task.ts` handles the Ajax call to the DELETE API. When the user hovers on a particular task, and clicks on the delete-bin icon that appears in the client, the DELETE API is triggered. * The DELETE API defined in `main.py` then executes the delete operation for the record in the _TodoItems_ table matching the ROWID and sends the response back to the client. * The `app.component.ts` removes the respective record (deleted task) from the To-Do list and displays the updated list in the client app upon a successful delete operation. **React web app** #### Configure a React web app The React web [client directory](https://docs.catalyst.zoho.com/en/cli/v1/project-directory-structure/client-directory) contains the following files: * The root directory of the client contains a `client-package.json` file which is a configuration file defining the name, version, and default home page of the client component. * The `app` folder contains two subfolders as per the default project structure of a React app: * The `public` folder is generally used to hold files that can be openly accessed by browsers through public URLs, such as icon files of the web app and `index.html`. * The `src` folder contains the application's source files that will be included in the `build` folder when we compile the React app. The `app` folder also contains the [`package.json`](https://docs.npmjs.com/files/package.json) dependency file, and a [`.gitignore`](https://git-scm.com/docs/gitignore) file. * The `public` folder contains the following files: * [Native React files](https://reactjs.org/docs/getting-started.html?#try-react) such as **`favicon.ico`, `logo192.png`, `manifest.json`, `logi512.png` and `robots.txt`**. These files will not be necessary for rendering the to-do list application. * `index.html`: The default entry point of the to-do list application. * The files present in the `src` folder include: * Native React files such as **`setupTests.js`, `index.js`, `reportWebVitals.js`, `logo.svg`, and `App.test.js`**. * `App.js`: Contains the logic of to-do list. * `index.css`: Contains the styling elements of the native elements. * `App.css`: Contains the styling elements of the application. * You will also be creating an additional file `helper.css`. This file will contain utility classes, that will be responsible for certain minor styling. We will be coding `index.html`, `App.js`, `helper.css`, `index.css`, and the `App.css` files. #### Install the Axios Package We will need the `axios` package to create a request from client to server. To install `axios`, navigate to the client directory (`app/`) and execute the following command: ```sh npm install axios ``` This will install the axios module and save the dependencies. ![catalyst_todo_react_axios.webp](https://catalyst-docs-development.zohostratus.in/images/tutorials/todo-list/catalyst_todo_react_axios.webp) > **Note**: Please go through the code given in this section to ensure you fully understand it. Copy the code given below and paste it in `index.html` located in client directory (`app/public/`) using an IDE and save the file. **index.html** ```xml <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="A Simple Catalyst Application." /> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap" rel="stylesheet" /> <title>To Do</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html> ``` Copy the respective code given below and paste it in `App.js`, `index.css`, `App.css` and `helper.css` respectively located in client directory (`app/src/`) using an IDE and save the file. **App.js** ```javascript import './App.css'; import './helper.css'; import axios from 'axios'; import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; //This segment contains the logic that displays each individual task present in the to-do list const Task = forwardRef(({ id, notes, index, removeTask }, ref) => { const [deleting, setDeleting] = useState(false); const [showOptions, setShowOptions] = useState(false); const onMouseEnter = useCallback(() => { setShowOptions(true); }, []); const onMouseLeave = useCallback(() => { setShowOptions(false); }, []); //Contains the logic to delete the taks from the Datastore const deleteTask = useCallback(() => { setDeleting(true); axios .delete(`/server/to_do_list_function/${id}`) //Ensure that 'to_do_list_function' is the package name of your function. .then((response) => { const { data: { todoItem: { id } } } = response.data; removeTask(id); }) .catch((err) => { console.log(err.response); }).finally(()=>{ setDeleting(false) }) }, [id, removeTask]); return ( <div className='task' ref={ref} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > <p className='task__no'>{index + 1 + ') '}</p> <p className='task__title'>{notes}</p> {deleting ? ( <div className='loader--xs'></div> ) : ( showOptions && ( <button onClick={deleteTask}> <svg className='task__btn' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' > <path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16' ></path> </svg> </button> ) )} </div> ); }); //This segment contains the logic for loading the application function App() { const observer = useRef(null); const [page, setPage] = useState(1); const [notes, setNotes] = useState(''); const [todoItems, setTodoItems] = useState([]); const [hasMore, setHasMore] = useState(false); const [submitting, setSubmitting] = useState(false); const [fetchState, setFetchState] = useState('init'); const onChange = useCallback((event) => { const { value } = event.target; setNotes(value); }, []); useEffect(() => { if (fetchState !== 'fetched') { axios .get('/server/to_do_list_function/all', { //Ensure that 'to_do_list_function' is the package name of your function. params: { page, perPage: 200 } //The parameters contain the start limit and the end limit of data (tasks) that can be fetched from the data store }) .then((response) => { const { data: { todoItems, hasMore } } = response.data; if (page === 1) { setTodoItems(todoItems); } else { setTodoItems((prev) => [ ...new Map( Array.from(prev) .concat(todoItems) .map((item) => [item.id, item]) ).values() ]); } setHasMore(hasMore); setFetchState('fetched'); }) .catch((err) => { console.log(err.response); }); } }, [fetchState, page]); const lastElement = useCallback( (node) => { if (fetchState !== 'fetched') { return; } if (observer.current) { observer.current.disconnect(); } observer.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore) { setPage((c) => c + 1); setFetchState('loading'); } }); if (node) { observer.current.observe(node); } }, [fetchState, hasMore] ); /// This segment contains the logic for creating a new task const createTodo = useCallback( (event) => { event.preventDefault(); setSubmitting(true); axios .post('/server/to_do_list_function/add', { //Ensure that 'to_do_list_function' is the package name of your function. notes }) .then((response) => { const { data: { todoItem } } = response.data; setNotes(''); setTodoItems((prev) => [{ ...todoItem }].concat(Array.from(prev))); }) .catch((err) => { console.log(err); }) .finally(() => { setSubmitting(false); }); }, [notes] ); //This segment contains the logic for deleting a task from the application const removeTask = useCallback((id) => { setTodoItems((prev) => Array.from(prev).filter((obj) => obj.id !== id)); }, []); return ( <div className='container'> {fetchState === 'init' ? ( <div className='dF aI-center jC-center h-inh'> <div className='loader--lg'></div> </div> ) : ( <> <div className='title-container px-20'> <p className='text-white text-28 font-700'>To Do</p> </div> <div className='create-container'> <form className='dF aI-center w-full' onSubmit={createTodo}> <input type='text' value={notes} onChange={onChange} placeholder='Enter a Task' className='input input--valid' readOnly={submitting} /> <button className='btn btn--primary ml-10' disabled={!notes.length || submitting} type='submit' > Create Task {submitting && ( <div className='btn--primary__loader ml-5'></div> )} </button> </form> </div> <div className='task-container'> {todoItems.length ? ( todoItems.map((item, index) => ( <Task key={item.id} {...item} ref={index === todoItems.length - 1 ? lastElement : null} index={index} removeTask={removeTask} /> )) ) : ( <div className='p-20 dF jC-center'> <p className='text-info text-16'> No tasks available, Create a new task. </p> </div> )} {fetchState === 'loading' && ( <div className='dF jC-center my-5'> <div className='loader--sm'></div> </div> )} </div> </> )} </div> ); } export default App; ``` > **Note**: The Python function package's name `to_do_list_function` has been added in the code in `App.js` in the lines **26**, **97** and **153**. If you have provided a different package name, ensure you replace accordingly. **index.css** ```css body, p { margin: 0; } * { box-sizing: border-box; font-family: 'Poppins', sans-serif; } input { margin: 0; resize: none; border: none; outline: none; -webkit-appearance: none; -moz-appearance: none; appearance: none; } button { padding: 0; border: none; outline: none; cursor: pointer; background: transparent; } ``` **App.css** ```css .container { height: 100vh; } .title-container { top: 0; left: 0; right: 0; z-index: 1; height: 4rem; display: flex; position: fixed; align-items: center; background: rgb(5, 150, 105); } .create-container { z-index: 1; top: 4rem; left: 0; right: 0; height: 5rem; padding: 20px; display: flex; position: fixed; background: #fff; align-items: center; } .task-container { padding-top: 9rem; } .task { display: flex; margin: 0 20px; font-size: 16px; padding: 12px 20px; align-items: center; border-radius: 5px; } .task:hover { background: rgba(5, 150, 105, 0.1); } .task__no { color: #111111; margin-right: 5px; } .task__title { flex: 1; margin-right: 5px; word-break: break-all; } .task__btn { width: 18px; height: 18px; opacity: 0.5; } .task__btn:hover { opacity: 1; } .input { width: 100%; font-size: 15px; padding: 12px 16px; border-radius: 5px; border: 1px solid #e0e0e0; } .input:focus { border: 1px solid rgb(5, 150, 105); box-shadow: 0px 0px 1px 1px rgb(5, 150, 105); } .input::-moz-placeholder { color: #919191; font-size: 15px; } .input:-ms-input-placeholder { color: #919191; font-size: 15px; } .input::placeholder { color: #919191; font-size: 15px; } .input:disabled { background: #f8f8f8; } .loader--lg { width: 60px; height: 60px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 7px solid rgb(5, 150, 105); border-right: 7px solid rgb(5, 150, 105); border-bottom: 7px solid transparent; animation: spin 0.8s linear infinite; } .loader--sm { width: 25px; height: 25px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 4px solid rgb(5, 150, 105); border-right: 4px solid rgb(5, 150, 105); border-bottom: 4px solid transparent; -webkit-animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite; } .loader--xs { width: 20px; height: 20px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 4px solid rgb(5, 150, 105); border-right: 4px solid rgb(5, 150, 105); border-bottom: 4px solid transparent; -webkit-animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite; } .btn { flex-shrink: 0; display: flex; cursor: pointer; font-size: 15px; font-weight: 500; padding: 12px 16px; align-items: center; border-radius: 5px; } .btn--primary { color: #fff; background: rgb(5, 150, 105); } .btn--primary__loader { width: 15px; height: 15px; margin: 0 5px; display: block; border-radius: 50%; box-sizing: border-box; border-top: 3px solid #fff; border-right: 3px solid #fff; border-bottom: 3px solid transparent; -webkit-animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite; } .btn--primary:disabled { background: rgba(5, 150, 105, 0.7); } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ``` In the (`app/src/`) directory, create a new file named (`helper.css`), then copy and paste the below code into it. **helper.css** ```css .my-5 { margin-top: 5px; margin-bottom: 5px; } .mr-5 { margin-right: 5px; } .ml-10 { margin-left: 10px; } .px-20 { padding-left: 20px; padding-right: 20px; } .p-20 { padding: 20px; } .w-full { width: 100%; } .text-28 { font-size: 28px; } .text-16 { font-size: 16px; } .text-white { color: #fff; } .text-info { color: #919191; } .dF { display: flex; } .aI-center { align-items: center; } .jC-center { justify-content: center; } .flex-1 { flex-grow: 1; } .h-inh { height: inherit; } .font-700 { font-weight: 700; } ``` The client directory is now configured. Let's quickly go over the working of the function and the client code: 1. **POST Operation** * When the user enters a to-do list item in the app and saves it, the `submit` event associated with the **Create Task** button triggers an Ajax call to the POST API. * The `App.js` in the client handles the Ajax operation and the URL, and it calls the POST API defined in the `main.py` function file. * The POST API defined in `main.py` inserts the data as a record in the _TodoItems_ table in the Data Store. The list item is inserted as the value of the _Notes_ column. * After a record insertion is done, the response (new task) will be added to the to-do list. 2. **GET Operation** * The `reload` event triggers an Ajax call to the GET API. The URL and the Ajax operation are handled in `App.js`. * The GET API defined in `main.py` obtains all the records from the Data Store by executing a ZCQL query. The ZCQL query contains the fetch operation along with the start-limit and the end-limit of the number of records that can be fetched. * The response containing the records (tasks) will be added to the existing to-do list. 3. **DELETE Operation** * The `App.js` handles the Ajax call to the DELETE API. When the user hovers on a particular task, and clicks on the appearing delete-bin icon in the client app, the DELETE API is triggered. * The DELETE API defined in `main.py` then executes the delete operation for the record in the _TodoItems_ table matching the ROWID and sends the response back to the client. * The `App.js` removes the respective record (deleted task) from the to-do list and displays the updated to-do list in the client app upon a successful delete operation.