--- title: "Configurar el cliente" url: https://docs.catalyst.zoho.com/es/tutorials/todo-list/python/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 del directorio del cliente" url: /es/cli/v1/project-directory-structure/client-directory --- # Configurar el cliente A continuación, configuremos el componente del cliente. Según su preferencia anterior, puede navegar a la sección correspondiente: **Aplicación web básica** #### Configurar una Basic web app 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 front-end. * El archivo `main.css` que contiene el código CSS para la aplicación del front-end. * 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** ```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(''); }; //API GET. Contiene la lógica para obtener los elementos existentes de la lista de tareas. function getTodos() { $.ajax({ url: `/server/to_do_list_function/all?page=${page}&perPage=200`, //Asegúrese de que 'to_do_list_function' 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/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) ); } } //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/to_do_list_function/add', //Asegúrese de que 'to_do_list_function' 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 del paquete de la función Python `to_do_list_function` se ha agregado en el código de `main.js` en las líneas **13** y **182**. Si proporcionó un nombre de paquete diferente, asegúrese de reemplazarlo correspondientemente. El directorio del cliente está ahora configurado. Repasemos rápidamente el funcionamiento de la función y el código del cliente: 1. **Operación POST** * Cuando el usuario ingresa un elemento de 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 maneja la operación Ajax y la URL, y llama a la API POST definida en el archivo de función `main.py`. * La API POST definida en `main.py` 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_. * Después de que se realiza 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 manejan en `main.js`. * La API GET definida en `main.py` 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` maneja 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 `main.py` luego ejecuta la operación de eliminación para el 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 respectivo (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 Angular web app 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` files, and the `dist`** directory. * 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 Datastore. getTodos = (): void => { axios .get('/server/to_do_list_function/all', { //Asegúrese de que 'to_do_list_function' 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); }); }; //API POST. Se está creando un nuevo elemento de la lista de tareas. createTodo = (): void => { this.submitting = true; axios .post(`/server/to_do_list_function/add`, { //Asegúrese de que 'to_do_list_function' 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(); } } ``` > **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 {} ``` 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/to_do_list_function/${this.id}`) //Asegúrese de que 'to_do_list_function' 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 del paquete de la función Python `to_do_list_function` se ha agregado en el código de `task.ts` en la línea **51**. Si proporcionó un nombre de paquete diferente, asegúrese de reemplazarlo correspondientemente. El directorio del cliente está ahora configurado. Repasemos rápidamente el funcionamiento de la función y el código del cliente: 1. **Operación POST** * Cuando el usuario ingresa un elemento de 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 maneja la operación Ajax y la URL, y llama a la API POST definida en el archivo de función `main.py`. * La API POST definida en `main.py` 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_. * Después de que se realiza 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 manejan en `app.component.ts`. * La API GET definida en `main.py` 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` maneja 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 `main.py` luego ejecuta la operación de eliminación para el 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 respectivo (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 React web app El [directorio del cliente](https://docs.catalyst.zoho.com/es/cli/v1/project-directory-structure/client-directory) web 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 usa generalmente para contener archivos que los navegadores pueden acceder abiertamente a través de URLs públicas, como archivos de íconos 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** ```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> ``` 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 Datastore const deleteTask = useCallback(() => { setDeleting(true); axios .delete(`/server/to_do_list_function/${id}`) //Asegúrese de que 'to_do_list_function' 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/to_do_list_function/all', { //Asegúrese de que 'to_do_list_function' sea el nombre del paquete de su función. 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] ); /// Este segmento contiene la lógica para crear una nueva tarea const createTodo = useCallback( (event) => { event.preventDefault(); setSubmitting(true); axios .post('/server/to_do_list_function/add', { //Asegúrese de que 'to_do_list_function' 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; ``` > **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); } } ``` En el directorio (`app/src/`), cree un nuevo archivo llamado (`helper.css`), luego copie y pegue el código a continuación en él. **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 de la función y el código del cliente: 1. **Operación POST** * Cuando el usuario ingresa un elemento de 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 maneja la operación Ajax y la URL, y llama a la API POST definida en el archivo de función `main.py`. * La API POST definida en `main.py` 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_. * Después de que se realiza 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 manejan en `App.js`. * La API GET definida en `main.py` 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` maneja 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 `main.py` luego ejecuta la operación de eliminación para el 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 respectivo (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.