Configure the Client
Next, let's configure the client component.
Based on your earlier preference, you can navigate to the relevant section:
Configure Basic web app client component
The 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.
Copy the code snippets given below and paste it in index.html, main.css, and main.js files respectively located in client/ directory using an IDE, then save the file.
View code for index.html
Copied
<!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>
View code for main.css
Copied
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; }
View code for main.js
Copied
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. If your Advanced I/O function is coded in the Java environment, replace "to_do_list_function" with "ToDoList". 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. If your Advanced I/O function is coded in the Java environment, replace "to_do_list_function" with "ToDoList". 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; }
The client directory is now configured.
Let's quickly go over the working of the function and the client code:
- 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 ToDoList.java or index.js function file.
- The POST API defined in ToDoList.java or index.js 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 ToDoList.
- 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 ToDoList.java or index.js 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 ToDoList
- 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 ToDoList.java or index.js 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 ToDoList and displays the updated ToDoList in the client app upon a successful delete operation.
You can now test the application. Next Step
Configure Angular web app client component
The Angular web 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 angular.json, karma.conf.js, tsconfig.app.json, tsconfig.json, tsconfig.spec.json files, and the dist directory are native to angular. You can find more information on these files and their purposes in the Angular documentation.
- The root directory of the client also contains the package.json dependency file, and a .gitignorefile.
- The src folder contains the following files and directories as per the default project structure of the Angular app:
- The favicon.ico, main.ts, polyfills.ts, and test.ts files along with the assets, and environment directories are native to Angular. You can find more information on these files and their purposes in the Angular documentation.
- index.html: The default entry point of the ToDoList application.
- style.css: Contains all the style elements of the ToDoList application.
- The files present in the src/app/ directory are:
- The app.component.css, and the app.component.spec.ts files that are native to Angular. You can find more information on these files and their purposes in the Angular documentation.
- app.component.html: Contains the HTML component of each event generated by the user.
- app.component.ts: Contains the logic of the ToDoList application.
- app.module.ts: Contains the meta of all the components used in building the ToDoList application.
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 src/app/ directory to create the task component:
$ ng generate component task
This will create a folder named task that contains:
- The task.component.css, and the task.component.spec.ts files are native to Angular. You can find more information on these files and their purposes in the Angular documentation.
- task.component.html: Contains the HTML component of each to-do task entered by the user.
- task.component.ts: Contains the logic of each to-do task entered by the user.
We will be coding the task.component.html and task.component.ts files as well.
Install 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:
$ npm install axios
This will install the axios module and save the dependencies.
You can now begin adding code in your files.
Copy the code given below and paste it in index.html and style.css files located in client directory (client/src/) respectively using an IDE and save the file.
View code for index.html
Copied
<!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>
View code for style.css
Copied
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.
View code for app.component.html
Copied
<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>
View code for app.component.ts
Copied
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. If your Advanced I/O function is coded in the Java environment, replace "to_do_list_function" with "ToDoList". 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. If your Advanced I/O function is coded in the Java environment, replace "to_do_list_function" with "ToDoList". 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(); } }
View code for app.module.ts
Copied
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { TaskComponent } from './task/task.component'; @NgModule({ declarations: [AppComponent, TaskComponent], imports: [BrowserModule, FormsModule], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
Copy the code given below and paste it in task.component.html and task.component.ts, files located in the client directory (client/src/app/task/) using an IDE and save the file.
View code for task.component.html
Copied
<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>
View code for task.component.ts
Copied
import axios from 'axios'; import { Component, EventEmitter, Input, Output, ElementRef, AfterViewInit, OnDestroy, } from '@angular/core'; @Component({ selector: 'app-task', templateUrl: './task.component.html', }) export class TaskComponent implements OnDestroy, AfterViewInit { @Input() id: string; @Input() notes: string; @Input() index: number; @Input() isLast: boolean; @Output() removeTodo: EventEmitter
; @Output() changePage: EventEmitter ; 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. If your Advanced I/O function is coded in the Java environment, replace "to_do_list_function" with "ToDoList". .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(); } } }
The client directory is now configured.
Let's quickly go over the working of the function and the client code:
- 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 ToDoList.java or index.js function file.
- The POST API defined in ToDoList.java or index.js 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 ToDoList.
- 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 ToDoList.java or index.js 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 ToDoList
- DELETE Operation
- When the user clicks on a list item in the client app, the DELETE API is triggered.
- The task.component.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 ToDoList.java or index.js 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.
You can now test the application. Next Step
Configure React web app client component
The React web 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 public folder contains the following files:
- The favicon.ico, logo192.png, manifest.json, logi512.png and the robots.txt files are native React files. These files will not be necessary for rendering the ToDoList application. You can find more information on the purpose of these files in the React documentation.
- index.html: The default entry point of the ToDoList application.
- The client-package.json configuration file.
- The files present in the src folder include:
- The setupTests.js, index.js, reportWebVitals.js, logo.svg, and the App.test.js are files that are native to React. These files will not be necessary for rendering the ToDoList application. You can find more information on the purpose of these files in the React documentation.
- App.js: Contains the logic of ToDoList.
- 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 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:
$ npm install axios
This will install the axios module and save the dependencies.
Copy the code given below and paste it in index.html located in client directory (app/public/) using an IDE and save the file.
View code for index.html
Copied
<!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.
View code for App.js
Copied
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. If your Advanced I/O function is coded in the Java environment, replace "to_do_list_function" with "ToDoList". .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. If your Advanced I/O function is coded in the Java environment, replace "to_do_list_function" with "ToDoList". 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. If your Advanced I/O function is coded in the Java environment, replace "to_do_list_function" with "ToDoList". 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;
View code for index.css
Copied
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; }
View code for App.css
Copied
.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); } }
View code for helper.css
Copied
.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:
- 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 ToDoList.java or index.js function file.
- The App.js in the client handles the Ajax operation and the URL, and it calls the POST API defined in the ToDoList.java or index.js function file.
- The POST API defined in ToDoList.java or index.js 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 ToDoList.
- 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 ToDoList.java or index.js 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 ToDoList.
- 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 ToDoList.java or index.js 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 ToDoList and displays the updated ToDoList in the client app upon a successful delete operation.