クライアントの設定
次に、クライアントコンポーネントを設定しましょう。
以前の選択に応じて、該当するセクションに移動してください:
Basicウェブアプリの設定
クライアントディレクトリには以下が含まれています:
- index.html フロントエンドアプリケーションのHTMLコードを含むファイル。
- main.css フロントエンドアプリケーションのCSSコードを含むファイル。
- main.js JavaScriptコードを含むファイル。
- client-package.json 設定ファイル
index.html、main.css、main.jsをコーディングします。
以下のコードスニペットをコピーし、IDEを使用してclient/ディレクトリにある各ファイルに貼り付けて、ファイルを保存してください。
<!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(’’);
};
//GET API。既存のToDoアイテムを取得するロジックを含みます。
function getTodos() {
$.ajax({
url: `/server/to_do_list_function/all?page=${page}&perPage=200`, //「to_do_list_function」がファンクションのパッケージ名であることを確認してください。
success: function (data) {
const {
data: { todoItems, hasMore }
} = data;
window.todoItems = [
...new Map(
Array.from(window.todoItems)
.concat(todoItems)
.map((item) => [item.id, item])
).values()
];
window.hasMore = hasMore;
renderTodo();
},
error: function (err) {
console.log(err);
},
complete: function () {
$(’#infinite-scroll-loader’).removeClass(‘dB’).addClass(‘dN’);
$(’#page-loader’).removeClass(‘dF’).addClass(‘dN’);
$(’#layout’).removeClass(‘dN’).addClass(‘dB’);
}
});
}
function onMouseEnter(element) {
const id = element.id;
const delBtn = $(`#${id}-del`);
if (delBtn && delBtn.attr('data-deleting')) {
delBtn.removeClass('dN').addClass('dB');
}
}
function onMouseLeave(element) {
const id = element.id;
const delBtn = $(`#${id}-del`);
if (delBtn && delBtn.attr(‘data-deleting’)) {
delBtn.removeClass(‘dB’).addClass(‘dN’);
}
}
function onNotesChange(element) {
toggleCreateTaskBtn($(`#${element.id}`).val());
}
function toggleCreateTaskBtn(value) {
if (value) {
$(’#create-task-btn’).attr(‘disabled’, false);
} else {
$(’#create-task-btn’).attr(‘disabled’, true);
}
}
// DELETE API。ToDoアイテムを削除するロジックを含みます。
function deleteTodo(id) {
$(`#${id}-del`).attr({
disabled: true,
‘data-deleting’: true,
class: ‘dN’
});
$(`#${id}-del-loader`).removeClass(‘dN’).addClass(‘dB’);
$.ajax({
method: ‘DELETE’,
url: `/server/to_do_list_function/${id}`,
success: function () {
todoItems = todoItems.filter((obj) => obj.id !== id);
renderTodo();
},
error: function (err) {
console.log(err);
$(`#${id}-del`).attr({
disabled: false,
‘data-deleting’: false,
class: ‘dB’
});
$(`#${id}-del-loader`).removeClass(‘dB’).addClass(‘dN’);
}
});
}
function renderTodo() {
if (!todoItems.length) {
$(’#tasks’).html(
`
<div class=‘p-20 dF jC-center’>
<p class=‘text-info text-16’>
No tasks available, Create a new task.
</p>
</div>
`
);
} else {
let html = ‘’;
todoItems.forEach((item, index) => {
html += `<div
class=‘task’
id=${item.id}
onmouseenter=“onMouseEnter(this)”
onMouseLeave=“onMouseLeave(this)”
>
<p class=‘task__no’>${index + 1 + ‘) ‘}</p>
<p class=‘task__title’>${item.notes}</p>
<div class=‘loader--xs dN’ id="${item.id}-del-loader" ></div>
<button id="${item.id + ‘-del’}" class=“dN” onclick=“deleteTodo(’${
item.id
}’)” data-deleting=false >
<svg
class=‘task__btn’
fill=‘none’
stroke=‘currentColor’
viewBox=‘0 0 24 24’
xmlns=‘http://www.w3.org/2000/svg'
>
<path
stroke-linecap=‘round’
stroke-linejoin=‘round’
stroke-width=‘2’
d=‘M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16’
></path>
</svg>
</button>
</div>`;
});
$(’#tasks’).html(html);
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
page += 1;
$(’#tasks’)
.append(`
<div class=“dF jC-center my-5” id=“infinite-scroll-loader”>
<div class=“loader--sm”></div>
</div>
`);
getTodos();
}
});
observer.observe(
document.getElementById(todoItems[todoItems.length - 1].id)
);
}
}
//POST API。ToDoアイテムを作成するロジックを含みます。
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’, //「to_do_list_function」がファンクションのパッケージ名であることを確認してください。
data: JSON.stringify({ notes }),
success: function (data) {
const {
data: { todoItem }
} = data;
todoItems = [todoItem].concat(todoItems);
renderTodo();
},
error: function (error) {
console.log(error);
},
complete: function () {
$(`#notes`)
.attr({
readOnly: false
})
.val(’’);
$(’#create-task-btn-loader’).removeClass(‘dB’).addClass(‘dN’);
}
});
return false;
}
クライアントディレクトリの設定が完了しました。
ファンクションとクライアントコードの動作を簡単に確認しましょう:
-
POSTオペレーション
- ユーザーがアプリでToDoリストアイテムを入力して保存すると、Create Taskボタンに関連付けられたsubmitイベントがPOST APIへのAjax呼び出しをトリガーします。
- クライアントのmain.jsがAjax操作とURLを処理し、index.jsファンクションファイルで定義されたPOST APIを呼び出します。
- index.jsで定義されたPOST APIは、Data Storeの_TodoItems_テーブルにレコードとしてデータを挿入します。リストアイテムは_Notes_カラムの値として挿入されます。
- レコードの挿入が完了すると、レスポンス(新しいタスク)がToDoリストに追加されます。
-
GETオペレーション
- reloadイベントがGET APIへのAjax呼び出しをトリガーします。URLとAjax操作はmain.jsで処理されます。
- index.jsで定義されたGET APIは、ZCQLクエリを実行してData Storeからすべてのレコードを取得します。ZCQLクエリには、取得できるレコード数の開始制限と終了制限を含むフェッチ操作が含まれています。
- レコード(タスク)を含むレスポンスが既存のToDoリストに追加されます。
-
DELETEオペレーション
- main.jsがDELETE APIへのAjax呼び出しを処理します。ユーザーが特定のタスクにカーソルを合わせ、クライアントアプリに表示される削除アイコンをクリックすると、DELETE APIがトリガーされます。
- index.jsで定義されたDELETE APIは、ROWIDに一致する_TodoItems_テーブルのレコードの削除操作を実行し、レスポンスをクライアントに返します。
- main.jsは、削除操作が成功すると、対応するレコード(削除されたタスク)をToDoリストから削除し、更新されたToDoリストをクライアントアプリに表示します。
Angularウェブアプリの設定
Angularウェブクライアントディレクトリには以下のファイルが含まれています:
-
クライアントのルートディレクトリには、クライアントコンポーネントの名前、バージョン、デフォルトホームページを定義する設定ファイルである client-package.jsonファイルが含まれています。
-
ネイティブAngularファイル(angular.json、 karma.conf.js、 tsconfig.app.json、 tsconfig.json、 tsconfig.spec.jsonファイル、および distディレクトリ)。
-
クライアントのルートディレクトリには、 package.json依存関係ファイルと .gitignorefileも含まれています。
-
srcフォルダには、Angularアプリのデフォルトプロジェクト構造に従って以下のファイルとディレクトリが含まれています:
- ネイティブAngularファイル(favicon.ico、main.ts、polyfills.ts、test.tsファイル、およびassetsディレクトリとenvironmentディレクトリ)。
- index.html: ToDoリストアプリケーションのデフォルトエントリーポイントです。
- styles.css: ToDoリストアプリケーションのすべてのスタイル要素を含みます。
-
src/app/ディレクトリに含まれるファイル:
-
ネイティブAngularファイル(app.component.cssと app.component.spec.ts)。
-
app.component.html: ユーザーが生成した各イベントのHTMLコンポーネントを含みます。
-
app.component.ts: ToDoリストアプリケーションのロジックを含みます。
-
app.module.ts: ToDoリストアプリケーションの構築に使用されるすべてのコンポーネントのメタ情報を含みます。
-
index.html、styles.css、 app.component.html、app.component.ts、および app.module.tsファイルをコーディングします。
Taskコンポーネントの作成
エンドユーザーが入力する各ToDoタスクに関連するロジックを含むtaskコンポーネントを作成する必要があります。client/src/app/ディレクトリで以下のコマンドを実行してtaskコンポーネントを作成します:
これにより、以下を含むtaskという名前のフォルダが作成されます:
- Angularネイティブのtask.cssとtask.spec.tsファイル。
- task.html: ユーザーが入力した各ToDoタスクのHTMLコンポーネントを含みます。
- task.ts: ユーザーが入力した各ToDoタスクのロジックを含みます。
task.htmlとtask.tsファイルも コーディングします。
Axiosパッケージのインストール
クライアントからサーバーへのリクエストを作成するためにaxiosパッケージも必要です。
axiosをインストールするには、クライアントディレクトリ(client/)に移動して以下のコマンドを実行します:
これによりaxiosモジュールがインストールされ、依存関係が保存されます。
これでファイルにコードを追加する準備が整いました。
以下のコードをコピーし、IDEを使用してクライアントディレクトリ(client/src/)にあるindex.htmlとstyles.cssファイルにそれぞれ貼り付けて、ファイルを保存してください。
<!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;
}
以下のコードをコピーし、IDEを使用してクライアントディレクトリ(client/src/app/)にあるapp.component.html、 app.component.ts、app.module.tsファイルにそれぞれ貼り付けて、ファイルを保存してください。
<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’;
}
//GET API。Data Storeから既存のToDoアイテムを取得しています。
getTodos = (): void => {
axios
.get(’/server/to_do_list_function/all’, { //「to_do_list_function」がファンクションのパッケージ名であることを確認してください。
params: {
page: this.page,
perPage: 200,
},
})
.then((response) => {
const {
data: { todoItems, hasMore },
} = response.data;
if (this.page === 1) {
this.todoItems = todoItems as Array<{ id: string; notes: string }>;
} else {
this.todoItems = [
...new Map(
this.todoItems.concat(todoItems).map((item) => [item.id, item])
).values(),
];
}
this.hasMore = hasMore;
this.fetchState = ‘fetched’;
})
.catch((err) => {
console.error(err.response.data);
});
};
//POST API。新しいToDoアイテムを作成しています。
createTodo = (): void => {
this.submitting = true;
axios
.post(`/server/to_do_list_function/add`, { //「to_do_list_function」がファンクションのパッケージ名であることを確認してください。
notes: this.notes,
})
.then((response) => {
const {
data: { todoItem },
} = response.data;
this.notes = ‘’;
this.todoItems = [{ ... todoItem }].concat(this.todoItems);
})
.catch((err) => {
console.error(err.response.data);
})
.finally(() => {
this.submitting = false;
});
};
removeTodo = (id: string): void => {
this.todoItems = this.todoItems.filter((obj) => obj.id !== id);
};
changePage = (): void => {
if (this.hasMore) {
this.page += 1;
this.fetchState = ’loading’;
this.getTodos();
}
};
ngOnInit() {
this.fetchState = ‘init’;
this.getTodos();
}
}
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 {}
以下のコードをコピーし、IDEを使用してクライアントディレクトリ(client/src/app/task/)にあるtask.htmlとtask.tsファイルに貼り付けて、ファイルを保存してください。
<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;
//DELETE API。Data StoreからToDoアイテムを削除する呼び出しがここで行われます。
axios
.delete(`/server/to_do_list_function/${this.id}`) //「to_do_list_function」がファンクションのパッケージ名であることを確認してください。
.then((response) => {
const {
data: {
todoItem: { id },
},
} = response.data;
this.removeTodo.emit(id);
})
.catch((err) => {
console.log(err.response.data);
})
.finally(() => {
this.deleting = false;
});
};
ngAfterViewInit() {
if (this.isLast && this.element) {
this.observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.changePage.emit();
}
});
this.observer.observe(this.element.nativeElement);
}
}
ngOnDestroy() {
if (this.observer) {
this.observer.disconnect();
}
}
}
クライアントディレクトリの設定が完了しました。
ファンクションとクライアントコードの動作を簡単に確認しましょう:
-
POSTオペレーション
- ユーザーがアプリでToDoリストアイテムを入力して保存すると、submitイベントCreate TaskボタンがPOST APIへのAjax呼び出しをトリガーします。
- クライアントのapp.component.tsがAjax操作とURLを処理し、index.jsファンクションファイルで定義されたPOST APIを呼び出します。
- index.jsで定義されたPOST APIは、Data Storeの_TodoItems_テーブルにレコードとしてデータを挿入します。リストアイテムは_Notes_カラムの値として挿入されます。
- レコードの挿入が完了すると、レスポンス(新しいタスク)がToDoリストに追加されます。
-
GETオペレーション
- reloadイベントがGET APIへのAjax呼び出しをトリガーします。URLとAjax操作はapp.component.tsで処理されます。
- index.jsで定義されたGET APIは、ZCQLクエリを実行してData Storeからすべてのレコードを取得します。ZCQLクエリには、取得できるレコード数の開始制限と終了制限を含むフェッチ操作が含まれています。
- レコード(タスク)を含むレスポンスが既存のToDoリストに追加されます。
-
DELETEオペレーション
- ユーザーがクライアントアプリでリストアイテムをクリックすると、DELETE APIがトリガーされます。
- task.tsがDELETE APIへのAjax呼び出しを処理します。ユーザーが特定のタスクにカーソルを合わせ、クライアントに表示される削除アイコンをクリックすると、DELETE APIがトリガーされます。
- index.jsで定義されたDELETE APIは、ROWIDに一致する_TodoItems_テーブルのレコードの削除操作を実行し、レスポンスをクライアントに返します。
- app.component.tsは、削除操作が成功すると、対応するレコード(削除されたタスク)をToDoリストから削除し、更新されたリストをクライアントアプリに表示します。
Reactウェブアプリの設定
Reactクライアントディレクトリには以下のファイルが含まれています:
-
クライアントのルートディレクトリには、クライアントコンポーネントの名前、バージョン、デフォルトホームページを定義する設定ファイルであるclient-package.jsonファイルが含まれています。
-
appフォルダには、Reactアプリのデフォルトプロジェクト構造に従って2つのサブフォルダが含まれています:
- publicフォルダは、一般的にウェブアプリのアイコンファイルやindex.htmlなど、ブラウザからパブリックURLを通じてオープンにアクセスできるファイルを保持するために使用されます。
- srcフォルダには、Reactアプリをコンパイルする際にbuildフォルダに含まれるアプリケーションのソースファイルが含まれています。
appフォルダには、package.json依存関係ファイルと.gitignoreファイルも含まれています。
-
publicフォルダには以下のファイルが含まれています:
- ネイティブReactファイル(favicon.ico、logo192.png、manifest.json、logi512.png、robots.txt)。これらのファイルはToDoリストアプリケーションのレンダリングには必要ありません。
- index.html: ToDoリストアプリケーションのデフォルトエントリーポイントです。
-
srcフォルダに含まれるファイル:
- ネイティブReactファイル(setupTests.js、index.js、reportWebVitals.js、logo.svg、App.test.js)。
- App.js: ToDoリストのロジックを含みます。
- index.css: ネイティブ要素のスタイル要素を含みます。
- App.css: アプリケーションのスタイル要素を含みます。
-
追加ファイルhelper.cssも作成します。このファイルには、特定の軽微なスタイリングを担当するユーティリティクラスが含まれます。
index.html、App.js、helper.css、index.css、およびApp.cssファイルをコーディングします。
Axiosパッケージのインストール
クライアントからサーバーへのリクエストを作成するためにaxiosパッケージが必要です。
axiosをインストールするには、クライアントディレクトリ(app/)に移動して以下のコマンドを実行します:
これによりaxiosモジュールがインストールされ、依存関係が保存されます。
以下のコードをコピーし、IDEを使用してクライアントディレクトリ(app/public/)にあるindex.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”
/>
<title>To Do</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id=“root”></div>
</body>
</html>
以下のそれぞれのコードをコピーして、クライアントディレクトリ(app/src/)にあるApp.js、index.css、App.css、helper.cssにそれぞれIDEを使用して貼り付け、ファイルを保存します。
import ‘./App.css’;
import ‘./helper.css’;
import axios from ‘axios’;
import { forwardRef, useCallback, useEffect, useRef, useState } from ‘react’;
//このセグメントには、ToDoリストに存在する各タスクを表示するロジックが含まれています
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);
}, []);
//Data Storeからタスクを削除するロジックを含みます
const deleteTask = useCallback(() => {
setDeleting(true);
axios
.delete(`/server/to_do_list_function/${id}`) //「to_do_list_function」がファンクションのパッケージ名であることを確認してください。
.then((response) => {
const {
data: {
todoItem: { id }
}
} = response.data;
removeTask(id);
})
.catch((err) => {
console.log(err.response);
}).finally(()=>{
setDeleting(false)
})
}, [id, removeTask]);
return (
<div
className=‘task’
ref={ref}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<p className=‘task__no’>{index + 1 + ‘) ‘}</p>
<p className=‘task__title’>{notes}</p>
{deleting ? (
<div className=‘loader--xs’></div>
) : (
showOptions && (
<button onClick={deleteTask}>
<svg
className=‘task__btn’
fill=‘none’
stroke=‘currentColor’
viewBox=‘0 0 24 24’
xmlns=‘http://www.w3.org/2000/svg'
>
<path
strokeLinecap=‘round’
strokeLinejoin=‘round’
strokeWidth=‘2’
d=‘M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16’
></path>
</svg>
</button>
)
)}
</div>
);
});
//このセグメントには、アプリケーションをロードするロジックが含まれています
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’, { //「to_do_list_function」がファンクションのパッケージ名であることを確認してください。
params: { page, perPage: 200 } //パラメータには、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]
);
/// このセグメントには、新しいタスクを作成するロジックが含まれています
const createTodo = useCallback(
(event) => {
event.preventDefault();
setSubmitting(true);
axios
.post(’/server/to_do_list_function/add’, { //「to_do_list_function」がファンクションのパッケージ名であることを確認してください。
notes
})
.then((response) => {
const {
data: { todoItem }
} = response.data;
setNotes(’’);
setTodoItems((prev) => [{ ...todoItem }].concat(Array.from(prev)));
})
.catch((err) => {
console.log(err);
})
.finally(() => {
setSubmitting(false);
});
},
[notes]
);
//このセグメントには、アプリケーションからタスクを削除するロジックが含まれています
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);
}
}
(app/src/)ディレクトリに(helper.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;
}
クライアントディレクトリの設定が完了しました。
ファンクションとクライアントコードの動作を簡単に確認しましょう:
-
POST操作
- ユーザーがアプリでToDoリストアイテムを入力して保存すると、Create Taskボタンに関連付けられたsubmitイベントがPOST APIへのAjax呼び出しをトリガーします。
- クライアントのApp.jsがAjax操作とURLを処理し、index.jsファンクションファイルで定義されたPOST APIを呼び出します。
- index.jsで定義されたPOST APIは、データをData Storeの_TodoItems_テーブルにレコードとして挿入します。リストアイテムは_Notes_カラムの値として挿入されます。
- レコードの挿入が完了すると、レスポンス(新しいタスク)がToDoリストに追加されます。
-
GET操作
- reloadイベントがGET APIへのAjax呼び出しをトリガーします。URLとAjax操作はApp.jsで処理されます。
- index.jsで定義されたGET APIは、ZCQLクエリを実行してData Storeからすべてのレコードを取得します。ZCQLクエリには、取得できるレコード数の開始制限と終了制限とともにフェッチ操作が含まれています。
- レコード(タスク)を含むレスポンスが既存のToDoリストに追加されます。
-
DELETE操作
- App.jsがDELETE APIへのAjax呼び出しを処理します。ユーザーが特定のタスクにカーソルを合わせ、クライアントアプリに表示される削除アイコンをクリックすると、DELETE APIがトリガーされます。
- index.jsで定義されたDELETE APIは、ROWIDに一致する_TodoItems_テーブルのレコードの削除操作を実行し、レスポンスをクライアントに返します。
- App.jsは、削除操作が成功すると、該当するレコード(削除されたタスク)をToDoリストから削除し、クライアントアプリで更新されたToDoリストを表示します。
最終更新日 2026-03-24 17:38:39 +0530 IST

