--- title: "Configurar el cliente" url: https://docs.catalyst.zoho.com/es/tutorials/todo-list/java/configure-client/ description: "Construya una aplicación web de lista de tareas básica, React o Angular con Catalyst Advanced I/O Function y Data Store para anotar y eliminar tareas." last_updated: 2026-11-04T10:10:44Z service: All Services type: tutorial related_links: - name: "Estructura de directorio del cliente" url: /es/cli/v1/project-directory-structure/client-directory --- # Configurar el cliente A continuación, vamos a configurar el componente del cliente. Según su preferencia anterior, puede navegar a la sección correspondiente: **Aplicación web básica** El [directorio del cliente](https://docs.catalyst.zoho.com/es/cli/v1/project-directory-structure/client-directory) contiene: * El archivo `index.html` que contiene el código HTML para la aplicación del frontend. * El archivo `main.css` que contiene el código CSS para la aplicación del frontend. * El archivo `main.js` que contiene el código JavaScript. * El archivo de configuración `client-package.json` Codificaremos `index.html`, `main.css` y `main.js`. > **Nota**: Revise el código proporcionado en esta sección para asegurarse de que lo comprende completamente. Copie los fragmentos de código proporcionados a continuación y péguelos en los archivos correspondientes ubicados en el directorio `client/` usando un IDE, luego guarde los archivos. **index.html** ```html <!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(''); }; //API GET. Contiene la lógica para obtener los elementos existentes de la lista de tareas. function getTodos() { $.ajax({ url: `/server/ToDoList/all?page=${page}&perPage=200`, //Asegúrese de que 'ToDoList' sea el nombre del paquete de su función. 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); } } // API DELETE. Contiene la lógica para eliminar un elemento de la lista de tareas. 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/ToDoList/${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) ); } } //API POST. Contiene la lógica para crear un elemento de la lista de tareas. 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/ToDoList/add', //Asegúrese de que 'ToDoList' sea el nombre del paquete de su función. 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; } ``` > **Nota**: El nombre de la función de Java `ToDoList` se ha agregado en el código de `main.js` en las líneas **13** y **182**. Si creó la función Advanced I/O en el entorno Node.js, debe reemplazar el nombre de la función de Java con el nombre de la función de Node.js. El directorio del cliente está ahora configurado. Repasemos rápidamente el funcionamiento del código de la función y del cliente: 1. **Operación POST** * Cuando el usuario ingresa un elemento en la lista de tareas en la aplicación y lo guarda, el evento `submit` asociado con el botón **Create Task** activa una llamada Ajax a la API POST. * El archivo `main.js` en el cliente gestiona la operación Ajax y la URL, y llama a la API POST definida en el archivo de función `ToDoList.java`. * La API POST definida en `ToDoList.java` inserta los datos como un registro en la tabla _TodoItems_ en el Data Store. El elemento de la lista se inserta como el valor de la columna _Notes_. * Una vez realizada la inserción del registro, la respuesta (nueva tarea) se agregará a la lista de tareas. 2. **Operación GET** * El evento `reload` activa una llamada Ajax a la API GET. La URL y la operación Ajax se gestionan en `main.js`. * La API GET definida en `ToDoList.java` obtiene todos los registros del Data Store ejecutando una consulta ZCQL. La consulta ZCQL contiene la operación de obtención junto con el límite inicial y el límite final del número de registros que se pueden obtener. * La respuesta que contiene los registros (tareas) se agregará a la lista de tareas existente. 3. **Operación DELETE** * El archivo `main.js` gestiona la llamada Ajax a la API DELETE. Cuando el usuario pasa el cursor sobre una tarea particular y hace clic en el icono de papelera que aparece en la aplicación del cliente, se activa la API DELETE. * La API DELETE definida en `ToDoList.java` ejecuta la operación de eliminación del registro en la tabla _TodoItems_ que coincide con el ROWID y envía la respuesta de vuelta al cliente. * El archivo `main.js` elimina el registro correspondiente (tarea eliminada) de la lista de tareas y muestra la lista de tareas actualizada en la aplicación del cliente tras una operación de eliminación exitosa. **Aplicación web Angular** ### Configurar una aplicación web Angular El directorio del cliente web Angular contiene los siguientes archivos: * El directorio raíz del cliente contiene un archivo [`client-package.json`](https://docs.catalyst.zoho.com/es/cli/v1/project-directory-structure/client-directory), que es un archivo de configuración que define el nombre, la versión y la página de inicio predeterminada del componente del cliente. * [Archivos nativos de Angular](https://angular.io/docs) como **`angular.json`, `karma.conf.js`, `tsconfig.app.json`, `tsconfig.json`, `tsconfig.spec.json`, y el directorio `dist`**. * El directorio raíz del cliente también contiene el archivo de dependencias [`package.json`](https://docs.npmjs.com/files/package.json), y un archivo [`.gitignorefile`](https://git-scm.com/docs/gitignore). * La carpeta `src` contiene los siguientes archivos y directorios según la estructura de proyecto predeterminada de la aplicación Angular: * Archivos nativos de Angular como **`favicon.ico`, `main.ts`, `polyfills.ts` y `test.ts`**, junto con los directorios de assets y environment. * `index.html`: El punto de entrada predeterminado de la aplicación de lista de tareas. * `styles.css`: Contiene todos los elementos de estilo de la aplicación de lista de tareas. * Los archivos presentes en el directorio `src/app/` son: * Archivos nativos de Angular como **`app.component.css` y `app.component.spec.ts`**. * `app.component.html`: Contiene el componente HTML de cada evento generado por el usuario. * `app.component.ts`: Contiene la lógica de la aplicación de lista de tareas. * `app.module.ts`: Contiene los metadatos de todos los componentes utilizados en la construcción de la aplicación de lista de tareas. Codificaremos los archivos `index.html`, `styles.css`, `app.component.html`, `app.component.ts` y `app.module.ts`. #### Crear un componente Task Debe crear un componente `task` que contenga la lógica asociada con cada tarea que el usuario final ingresa. Ejecute el siguiente comando en el directorio **`client/src/app/`** para crear el componente `task`: ```sh ng generate component task ``` Esto creará una carpeta llamada `task` que contiene: * Los archivos `task.css` y `task.spec.ts` nativos de Angular. * `task.html`: Contiene el componente HTML de cada tarea ingresada por el usuario. * `task.ts`: Contiene la lógica de cada tarea ingresada por el usuario. También codificaremos los archivos `task.html` y `task.ts`. #### Instalar el paquete Axios También necesitaremos el paquete `axios` para crear una solicitud del cliente al servidor. Para instalar `axios`, navegue al directorio del cliente (`client/`) y ejecute el siguiente comando: ```sh npm install axios ``` Esto instalará el módulo axios y guardará las dependencias. ![catalyst_todo_angular_axios](https://catalyst-docs-development.zohostratus.in/images/tutorials/todo-list/catalyst_todo_angular_axios.webp) Ahora puede comenzar a agregar código en sus archivos. > **Nota**: Revise el código proporcionado en esta sección para asegurarse de que lo comprende completamente. Copie el código proporcionado a continuación y péguelo en los archivos `index.html` y `styles.css` ubicados en el directorio del cliente (`client/src/`) respectivamente usando un IDE y guarde el archivo. **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; } ``` Copie el código proporcionado a continuación y péguelo en los archivos `app.component.html`, `app.component.ts`, `app.module.ts` respectivamente ubicados en el directorio del cliente (`client/src/app/`) usando un IDE y guarde el archivo. **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'; } //API GET. Los elementos existentes de la lista de tareas se están obteniendo del Data Store. getTodos = (): void => { axios .get('/server/ToDoList/all', { //Asegúrese de que 'ToDoList' sea el nombre del paquete de su función. 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 ?? err); }); }; //API POST. Se está creando un nuevo elemento de la lista de tareas. createTodo = (): void => { this.submitting = true; axios .post('/server/ToDoList/add', { //Asegúrese de que 'ToDoList' sea el nombre del paquete de su función. 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(); } } ``` > **Nota**: El nombre de la función de Java `ToDoList` se ha agregado en el código de `app.component.ts` en las líneas **33** y **65**. Si creó la función Advanced I/O en el entorno Node.js, debe reemplazar el nombre de la función de Java con el nombre de la función de Node.js. **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 {} ``` Copie el código proporcionado a continuación y péguelo en los archivos `task.html` y `task.ts`, ubicados en el directorio del cliente (`client/src/app/task/`) usando un IDE y guarde el archivo. **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; //API DELETE. La llamada para eliminar el elemento de la lista de tareas del Data Store ocurre aquí. axios .delete(`/server/ToDoList/${this.id}`) //Asegúrese de que 'ToDoList' sea el nombre del paquete de su función. .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(); } } } ``` > **Nota**: El nombre de la función de Java `ToDoList` se ha agregado en el código de `task.ts` en la línea **51**. Si creó la función Advanced I/O en el entorno Node.js, debe reemplazar el nombre de la función de Java con el nombre de la función de Node.js. El directorio del cliente está ahora configurado. Repasemos rápidamente el funcionamiento del código de la función y del cliente: 1. **Operación POST** * Cuando el usuario ingresa un elemento en la lista de tareas en la aplicación y lo guarda, el evento `submit` del botón **Create Task** activa una llamada Ajax a la API POST. * El archivo `app.component.ts` en el cliente gestiona la operación Ajax y la URL, y llama a la API POST definida en el archivo de función `ToDoList.java`. * La API POST definida en `ToDoList.java` inserta los datos como un registro en la tabla _TodoItems_ en el Data Store. El elemento de la lista se inserta como el valor de la columna _Notes_. * Una vez realizada la inserción del registro, la respuesta (nueva tarea) se agregará a la lista de tareas. 2. **Operación GET** * El evento `reload` activa una llamada Ajax a la API GET. La URL y la operación Ajax se gestionan en `app.component.ts`. * La API GET definida en `ToDoList.java` obtiene todos los registros del Data Store ejecutando una consulta ZCQL. La consulta ZCQL contiene la operación de obtención junto con el límite inicial y el límite final del número de registros que se pueden obtener. * La respuesta que contiene los registros (tareas) se agregará a la lista de tareas existente. 3. **Operación DELETE** * Cuando el usuario hace clic en un elemento de la lista en la aplicación del cliente, se activa la API DELETE. * El archivo `task.ts` gestiona la llamada Ajax a la API DELETE. Cuando el usuario pasa el cursor sobre una tarea particular y hace clic en el icono de papelera que aparece en el cliente, se activa la API DELETE. * La API DELETE definida en `ToDoList.java` ejecuta la operación de eliminación del registro en la tabla _TodoItems_ que coincide con el ROWID y envía la respuesta de vuelta al cliente. * El archivo `app.component.ts` elimina el registro correspondiente (tarea eliminada) de la lista de tareas y muestra la lista actualizada en la aplicación del cliente tras una operación de eliminación exitosa. **Aplicación web React** ### Configurar una aplicación web React El directorio del cliente React contiene los siguientes archivos: * El directorio raíz del cliente contiene un archivo `client-package.json` que es un archivo de configuración que define el nombre, la versión y la página de inicio predeterminada del componente del cliente. * La carpeta `app` contiene dos subcarpetas según la estructura de proyecto predeterminada de una aplicación React: * La carpeta `public` se utiliza generalmente para almacenar archivos que pueden ser accedidos abiertamente por los navegadores a través de URLs públicas, como archivos de iconos de la aplicación web e `index.html`. * La carpeta `src` contiene los archivos fuente de la aplicación que se incluirán en la carpeta `build` cuando compilemos la aplicación React. La carpeta `app` también contiene el archivo de dependencias [`package.json`](https://docs.npmjs.com/files/package.json), y un archivo [`.gitignore`](https://git-scm.com/docs/gitignore). * La carpeta `public` contiene los siguientes archivos: * [Archivos nativos de React](https://reactjs.org/docs/getting-started.html?#try-react) como **`favicon.ico`, `logo192.png`, `manifest.json`, `logi512.png` y `robots.txt`**. Estos archivos no serán necesarios para renderizar la aplicación de lista de tareas. * `index.html`: El punto de entrada predeterminado de la aplicación de lista de tareas. * Los archivos presentes en la carpeta `src` incluyen: * Archivos nativos de React como **`setupTests.js`, `index.js`, `reportWebVitals.js`, `logo.svg` y `App.test.js`**. * `App.js`: Contiene la lógica de la lista de tareas. * `index.css`: Contiene los elementos de estilo de los elementos nativos. * `App.css`: Contiene los elementos de estilo de la aplicación. * También creará un archivo adicional `helper.css`. Este archivo contendrá clases de utilidad que serán responsables de ciertos estilos menores. Codificaremos los archivos `index.html`, `App.js`, `helper.css`, `index.css` y `App.css`. #### Instalar el paquete Axios Necesitaremos el paquete `axios` para crear una solicitud del cliente al servidor. Para instalar `axios`, navegue al directorio del cliente (`app/`) y ejecute el siguiente comando: ```sh npm install axios ``` Esto instalará el módulo axios y guardará las dependencias. ![catalyst_todo_react_axios.webp](https://catalyst-docs-development.zohostratus.in/images/tutorials/todo-list/catalyst_todo_react_axios.webp) > **Nota**: Revise el código proporcionado en esta sección para asegurarse de que lo comprende completamente. Copie el código proporcionado a continuación y péguelo en `index.html` ubicado en el directorio del cliente (`app/public/`) usando un IDE y guarde el archivo. **index.html** ```java <!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> ``` Copie el código respectivo proporcionado a continuación y péguelo en los archivos `App.js`, `index.css`, `App.css` y `helper.css` respectivamente ubicados en el directorio del cliente (`app/src/`) usando un IDE y guarde el archivo. **App.js** ```javascript import './App.css'; import './helper.css'; import axios from 'axios'; import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; //Este segmento contiene la lógica que muestra cada tarea individual presente en la lista de tareas 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); }, []); //Contiene la lógica para eliminar las tareas del Data Store const deleteTask = useCallback(() => { setDeleting(true); axios .delete(`/server/ToDoList/${id}`) //Asegúrese de que 'ToDoList' sea el nombre del paquete de su función. .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> ); }); //Este segmento contiene la lógica para cargar la aplicación 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/ToDoList/all', { //Asegúrese de que 'ToDoList' sea el nombre del paquete de su función. params: { page, perPage: 200 } //Los parámetros contienen el límite inicial y el límite final de datos (tareas) que se pueden obtener del 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] ); /// Este segmento contiene la lógica para crear una nueva tarea const createTodo = useCallback( (event) => { event.preventDefault(); setSubmitting(true); axios .post('/server/ToDoList/add', { //Asegúrese de que 'ToDoList' sea el nombre del paquete de su función. 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] ); //Este segmento contiene la lógica para eliminar una tarea de la aplicación 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; ``` > **Nota**: El nombre de la función de Java `ToDoList` se ha agregado en el código de `App.js` en las líneas **26**, **97** y **153**. Si creó la función Advanced I/O en el entorno Node.js, debe reemplazar el nombre de la función de Java con el nombre de la función de Node.js. **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; } ``` El directorio del cliente está ahora configurado. Repasemos rápidamente el funcionamiento del código de la función y del cliente: 1. **Operación POST** * Cuando el usuario ingresa un elemento en la lista de tareas en la aplicación y lo guarda, el evento `submit` asociado con el botón **Create Task** activa una llamada Ajax a la API POST. * El archivo `App.js` en el cliente gestiona la operación Ajax y la URL, y llama a la API POST definida en el archivo de función `ToDoList.java`. * La API POST definida en `ToDoList.java` inserta los datos como un registro en la tabla _TodoItems_ en el Data Store. El elemento de la lista se inserta como el valor de la columna _Notes_. * Una vez realizada la inserción del registro, la respuesta (nueva tarea) se agregará a la lista de tareas. 2. **Operación GET** * El evento `reload` activa una llamada Ajax a la API GET. La URL y la operación Ajax se gestionan en `App.js`. * La API GET definida en `ToDoList.java` obtiene todos los registros del Data Store ejecutando una consulta ZCQL. La consulta ZCQL contiene la operación de obtención junto con el límite inicial y el límite final del número de registros que se pueden obtener. * La respuesta que contiene los registros (tareas) se agregará a la lista de tareas existente. 3. **Operación DELETE** * El archivo `App.js` gestiona la llamada Ajax a la API DELETE. Cuando el usuario pasa el cursor sobre una tarea particular y hace clic en el icono de papelera que aparece en la aplicación del cliente, se activa la API DELETE. * La API DELETE definida en `ToDoList.java` ejecuta la operación de eliminación del registro en la tabla _TodoItems_ que coincide con el ROWID y envía la respuesta de vuelta al cliente. * El archivo `App.js` elimina el registro correspondiente (tarea eliminada) de la lista de tareas y muestra la lista de tareas actualizada en la aplicación del cliente tras una operación de eliminación exitosa.