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:
Configurar una aplicación web básica
El directorio del cliente 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.
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.
<!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>
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;
}
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;
}
El directorio del cliente está ahora configurado.
Repasemos rápidamente el funcionamiento del código de la función y del cliente:
-
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 index.js.
- La API POST definida en index.js 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.
-
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 index.js 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.
-
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 index.js 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.
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, 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 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, y un archivo .gitignorefile.
-
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:
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:
Esto instalará el módulo axios y guardará las dependencias.
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.
<!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>
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.
<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>
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();
}
}
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.
<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>
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();
}
}
}
El directorio del cliente está ahora configurado.
Repasemos rápidamente el funcionamiento del código de la función y del cliente:
-
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 index.js.
- La API POST definida en index.js 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.
-
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 index.js 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.
-
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 index.js 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.
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, y un archivo .gitignore.
-
La carpeta public contiene los siguientes archivos:
- Archivos nativos de 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:
Esto instalará el módulo axios y guardará las dependencias.
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.
<!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.
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/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 } //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/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;
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;
}
.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.
.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:
-
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 index.js.
- La API POST definida en index.js 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.
-
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 index.js 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.
-
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 index.js 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.
Última actualización 2026-03-24 17:38:39 +0530 IST

