ノードを使用して軽量の請求アプリを構築する方法:ユーザーインターフェイス
序章
前のチュートリアルでは、請求書発行アプリケーション用のバックエンドサーバーを構築しました。 このチュートリアルでは、ユーザーインターフェイスと呼ばれる、ユーザーが操作するアプリケーションの部分を構築します。
注:これは3部構成のシリーズのパート2です。 最初のチュートリアルは、ノードを使用して軽量の請求書作成アプリを構築する方法:データベースとAPIです。 3番目のチュートリアルは、 Vueとノードを使用して軽量の請求書作成アプリを構築する方法:JWT認証と請求書の送信です。
このチュートリアルのユーザーインターフェイスはVueで構築され、ユーザーがログインして請求書を表示および作成できるようにします。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- Node.jsはローカルにインストールされます。これは、Node.jsのインストール方法とローカル開発環境の作成に従って実行できます。
このチュートリアルは、Node v16.1.0、npm
v7.12.1、Vue v2.6.11、Vue Router v3.2.0、axios
v0.21.1、およびBootstrapv5.0.1で検証されました。
ステップ1—プロジェクトの設定
@ vue / cli を使用して、新しいVue.jsプロジェクトを作成できます。
注:この新しいプロジェクトディレクトリは、前のチュートリアルで作成したinvoicing-app
ディレクトリの隣に配置できるはずです。 これにより、server
とclient
を分離する一般的な方法が導入されます。
ターミナルウィンドウで、次のコマンドを使用します。
- npx @vue/cli create --inlinePreset='{ "useConfigFiles": false, "plugins": { "@vue/cli-plugin-babel": {}, "@vue/cli-plugin-eslint": { "config": "base", "lintOn": ["save"] } }, "router": true, "routerHistoryMode": true }' invoicing-app-frontend
これは、 VueRouterでVue.jsプロジェクトを作成するためのインラインプリセット構成を使用します。
新しく作成されたプロジェクトディレクトリに移動します。
- cd invoicing-app-frontend
プロジェクトを開始して、エラーがないことを確認します。
- npm run serve
Webブラウザでローカルアプリ(通常はlocalhost:8080
)にアクセスすると、"Welcome to Your Vue.js App"
メッセージが表示されます。
これにより、この記事で構築するサンプルVue
プロジェクトが作成されます。
この請求書発行アプリケーションのフロントエンドでは、バックエンドサーバーに対して多くの要求が行われます。
これを実現するために、axiosを使用します。 axios
をインストールするには、プロジェクトディレクトリで次のコマンドを実行します。
- npm install axios@0.21.1
アプリケーションでデフォルトのスタイルを使用できるようにするには、Bootstrap
を使用します。
まず、コードエディタでpublic/index.html
ファイルを開きます。
Bootstrap用のCDNでホストされているCSSファイルをドキュメントのhead
に追加します。
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
PopperおよびBootstrap用のCDNでホストされているJavaScriptファイルをドキュメントのhead
に追加します。
<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-Atwg2Pkwv9vp0ygtn1JAojH0nYbwNJLPhwyoVbhoPwBhjQPR5VtM2+xf0Uwh9KtT" crossorigin="anonymous"></script>
App.vue
の内容を次のコード行に置き換えることができます。
<template>
<div id="app">
<router-view/>
</div>
</template>
また、自動生成されたsrc/views/Home.vue
、src/views/About.vue
、src/components/HelloWorld.vue
ファイルは無視または削除できます。
この時点で、AxiosとBootstrapを使用した新しいVueプロジェクトがあります。
ステップ2—Vueルーターの設定
このアプリケーションでは、2つの主要なルートがあります。
/
ログインページをレンダリングします/dashboard
ユーザーダッシュボードをレンダリングします
これらのルートを構成するには、src/router/index.js
を開き、次のコード行で更新します。
import Vue from 'vue'
import VueRouter from 'vue-router'
import SignUp from '@/components/SignUp'
import Dashboard from '@/components/Dashboard'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'SignUp',
component: SignUp
},
{
path: '/dashboard',
name: 'Dashboard',
component: Dashboard
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
これは、ユーザーがアプリケーションにアクセスしたときにユーザーに表示する必要のあるコンポーネントを指定します。
ステップ3—コンポーネントの作成
コンポーネントを使用すると、アプリケーションのフロントエンドをよりモジュール化して再利用できるようになります。 このアプリケーションには、次のコンポーネントが含まれます。
- ヘッダ
- ナビゲーション
- サインアップ(およびサインイン)
- ダッシュボード
- 請求書を作成する
- 請求書を表示する
ヘッダーコンポーネントの作成
Header
コンポーネントは、アプリケーションの名前を表示し、ユーザーがサインインしている場合はNavigation
を表示します。
Header.vue
ファイルをsrc/components
ディレクトリに作成します。 コンポーネントファイルには、次のコード行があります。
<template>
<nav class="navbar navbar-light bg-light">
<div class="navbar-brand m-0 p-3 h1 align-self-start">{{title}}</div>
<template v-if="user != null">
<Navigation v-bind:name="user.name" v-bind:company="user.company_name"/>
</template>
</nav>
</template>
<script>
import Navigation from './Navigation'
export default {
name: "Header",
props : ["user"],
components: {
Navigation
},
data() {
return {
title: "Invoicing App",
};
}
};
</script>
ヘッダーコンポーネントには、user
と呼ばれる単一のprop
があります。 このprop
は、ヘッダーコンポーネントを使用するすべてのコンポーネントによって渡されます。 ヘッダーのテンプレートでは、Navigation
コンポーネントがインポートされ、条件付きレンダリングを使用して、Navigation
を表示するかどうかが決定されます。
ナビゲーションコンポーネントの作成
Navigation
コンポーネントは、さまざまなアクションのリンクを格納するサイドバーです。
/src/components
ディレクトリに新しいNavigation.vue
コンポーネントを作成します。 コンポーネントには次のテンプレートがあります。
<template>
<div class="flex-grow-1">
<div class="navbar navbar-expand-lg">
<ul class="navbar-nav flex-grow-1 flex-row">
<li class="nav-item">
<a class="nav-link" v-on:click="setActive('create')">Create Invoice</a>
</li>
<li class="nav-item">
<a class="nav-link" v-on:click="setActive('view')">View Invoices</a>
</li>
</ul>
</div>
<div class="navbar-text"><em>Company: {{ company }}</em></div>
<div class="navbar-text h3">Welcome, {{ name }}</div>
</div>
</template>
...
次に、コードエディタでNavigation.vue
ファイルを開き、次のコード行を追加します。
...
<script>
export default {
name: "Navigation",
props: ["name", "company"],
methods: {
setActive(option) {
this.$parent.$parent.isactive = option;
},
}
};
</script>
コンポーネントは、ユーザー名と会社名の2つのprops
で作成されます。 setActive
メソッドは、ユーザーがナビゲーションリンクをクリックすると、Navigation
コンポーネントの親(この場合はDashboard
)を呼び出すコンポーネントを更新します。
サインアップコンポーネントの作成
SignUp
コンポーネントには、サインアップおよびサインインフォームが格納されています。 /src/components
ディレクトリに新しいファイルを作成します。
まず、コンポーネントを作成します。
<template>
<div class="container">
<Header/>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="login-tab" data-bs-toggle="tab" data-bs-target="#login" type="button" role="tab" aria-controls="login" aria-selected="true">Login</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button" role="tab" aria-controls="register" aria-selected="false">Register</button>
</li>
</ul>
<div class="tab-content p-3">
...
</div>
</div>
</template>
<script>
import axios from "axios"
import Header from "./Header"
export default {
name: "SignUp",
components: {
Header
},
data() {
return {
model: {
name: "",
email: "",
password: "",
c_password: "",
company_name: ""
},
loading: "",
status: ""
};
},
methods: {
...
}
}
</script>
Header
コンポーネントがインポートされ、コンポーネントのデータプロパティも指定されます。
次に、データが送信されたときに何が起こるかを処理するメソッドを作成します。
...
methods: {
validate() {
// checks to ensure passwords match
if (this.model.password != this.model.c_password) {
return false;
}
return true;
},
...
}
...
validate()
メソッドは、ユーザーから送信されたデータが要件を満たしていることを確認するためのチェックを実行します。
...
methods: {
...
register() {
const formData = new FormData();
let valid = this.validate();
if (valid) {
formData.append("name", this.model.name);
formData.append("email", this.model.email);
formData.append("company_name", this.model.company_name);
formData.append("password", this.model.password);
this.loading = "Registering you, please wait";
// Post to server
axios.post("http://localhost:3128/register", formData).then(res => {
// Post a status message
this.loading = "";
if (res.data.status == true) {
// now send the user to the next route
this.$router.push({
name: "Dashboard",
params: { user: res.data.user }
});
} else {
this.status = res.data.message;
}
});
} else {
alert("Passwords do not match");
}
},
...
}
...
コンポーネントのregister
メソッドは、ユーザーが新しいアカウントを登録しようとしたときのアクションを処理します。 まず、validate
メソッドを使用してデータを検証します。 次に、すべての基準が満たされると、formData
を使用してデータを送信できるように準備されます。
また、コンポーネントのloading
プロパティを定義して、フォームが処理されていることをユーザーに通知します。 最後に、POST
リクエストがaxios
を使用してバックエンドサーバーに送信されます。 サーバーからステータスがtrue
の応答を受信すると、ユーザーはダッシュボードに移動します。 それ以外の場合は、エラーメッセージがユーザーに表示されます。
...
methods: {
...
login() {
const formData = new FormData();
formData.append("email", this.model.email);
formData.append("password", this.model.password);
this.loading = "Logging In";
// Post to server
axios.post("http://localhost:3128/login", formData).then(res => {
// Post a status message
this.loading = "";
if (res.data.status == true) {
// now send the user to the next route
this.$router.push({
name: "Dashboard",
params: { user: res.data.user }
});
} else {
this.status = res.data.message;
}
});
}
}
...
login
メソッドは、register
メソッドに似ています。 データは準備され、ユーザーを認証するためにバックエンドサーバーに送信されます。 ユーザーが存在し、詳細が一致する場合、ユーザーはダッシュボードに移動します。
次に、登録用のテンプレートを見てください。
<template>
<div class="container">
...
<div class="tab-content p-3">
<div id="login" class="tab-pane fade show active" role="tabpanel" aria-labelledby="login-tab">
<div class="row">
<div class="col-md-12">
<form @submit.prevent="login">
<div class="form-group mb-3">
<label for="login-email" class="label-form">Email:</label>
<input id="login-email" type="email" required class="form-control" placeholder="[email protected]" v-model="model.email">
</div>
<div class="form-group mb-3">
<label for="login-password" class="label-form">Password:</label>
<input id="login-password" type="password" required class="form-control" placeholder="Password" v-model="model.password">
</div>
<div class="form-group">
<button class="btn btn-primary">Log In</button>
{{ loading }}
{{ status }}
</div>
</form>
</div>
</div>
</div>
...
</div>
</div>
</template>
ログインフォームは上に表示されており、入力フィールドは、コンポーネントの作成時に指定されたそれぞれのデータプロパティにリンクされています。 フォームの送信ボタンをクリックすると、コンポーネントのlogin
メソッドが呼び出されます。
通常、フォームの送信ボタンをクリックすると、フォームはGET
またはPOST
リクエストを介して送信されます。 これを使用する代わりに、フォームの作成時に<form @submit.prevent="login">
を追加して、デフォルトの動作をオーバーライドし、ログイン関数を呼び出すように指定しました。
登録フォームも次のようになります。
<template>
<div class="container">
...
<div class="tab-content p-3">
...
<div id="register" class="tab-pane fade" role="tabpanel" aria-labelledby="register-tab">
<div class="row">
<div class="col-md-12">
<form @submit.prevent="register">
<div class="form-group mb-3">
<label for="register-name" class="label-form">Name:</label>
<input id="register-name" type="text" required class="form-control" placeholder="Full Name" v-model="model.name">
</div>
<div class="form-group mb-3">
<label for="register-email" class="label-form">Email:</label>
<input id="register-email" type="email" required class="form-control" placeholder="[email protected]" v-model="model.email">
</div>
<div class="form-group mb-3">
<label for="register-company" class="label-form">Company Name:</label>
<input id="register-company" type="text" required class="form-control" placeholder="Company Name" v-model="model.company_name">
</div>
<div class="form-group mb-3">
<label for="register-password" class="label-form">Password:</label>
<input id="register-password" type="password" required class="form-control" placeholder="Password" v-model="model.password">
</div>
<div class="form-group mb-3">
<label for="register-confirm" class="label-form">Confirm Password:</label>
<input id="register-confirm" type="password" required class="form-control" placeholder="Confirm Password" v-model="model.c_password">
</div>
<div class="form-group mb-3">
<button class="btn btn-primary">Register</button>
{{ loading }}
{{ status }}
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
@submit.prevent
は、送信ボタンがクリックされたときにregister
メソッドを呼び出すためにもここで使用されます。
次に、次のコマンドを使用して開発サーバーを実行します。
- npm run serve
ブラウザでlocalhost:8080
にアクセスして、新しく作成されたログインおよび登録ページを確認してください。
注:ユーザーインターフェイスを試すときは、invoicing-app
サーバーを実行している必要があります。 さらに、Access-Control-Allow-Origin
ヘッダーを設定することで対処する必要があるCORS(クロスオリジンリソースシェアリング)エラーが発生する場合があります。
ログインして新しいユーザーを登録してみてください。
ダッシュボードコンポーネントの作成
ユーザーが/dashboard
ルートにルーティングされると、ダッシュボードコンポーネントが表示されます。 デフォルトでは、Header
およびCreateInvoice
コンポーネントが表示されます。
Dashboard.vue
ファイルをsrc/components
ディレクトリに作成します。 コンポーネントには次のコード行があります。
<template>
<div class="container">
<Header v-bind:user="user"/>
<template v-if="this.isactive == 'create'">
<CreateInvoice />
</template>
<template v-else>
<ViewInvoices />
</template>
</div>
</template>
...
テンプレートの下に、次のコード行を追加します。
...
<script>
import Header from "./Header";
import CreateInvoice from "./CreateInvoice";
import ViewInvoices from "./ViewInvoices";
export default {
name: "Dashboard",
components: {
Header,
CreateInvoice,
ViewInvoices,
},
data() {
return {
isactive: 'create',
title: "Invoicing App",
user : (this.$route.params.user) ? this.$route.params.user : null
};
}
};
</script>
CreateInvoiceコンポーネントの作成
CreateInvoice
コンポーネントには、新しい請求書を作成するために必要なフォームが含まれています。 src/components
ディレクトリに新しいファイルを作成します。
CreateInvoice
コンポーネントを次のように編集します。
<template>
<div class="container">
<div class="tab-pane p-3 fade show active">
<div class="row">
<div class="col-md-12">
<h3>Enter details below to create invoice</h3>
<form @submit.prevent="onSubmit">
<div class="form-group mb-3">
<label for="create-invoice-name" class="form-label">Invoice Name:</label>
<input id="create-invoice-name" type="text" required class="form-control" placeholder="Invoice Name" v-model="invoice.name">
</div>
<div class="form-group mb-3">
Invoice Price: <span>${{ invoice.total_price }}</span>
</div>
...
</form>
</div>
</div>
</div>
</div>
</template>
これにより、請求書の名前を受け入れ、請求書の合計金額を表示するフォームが作成されます。 合計金額は、請求書の個々のトランザクションの価格を合計することによって得られます。
トランザクションが請求書に追加される方法を見てみましょう。
...
<form @submit.prevent="onSubmit">
...
<hr />
<h3>Transactions </h3>
<div class="form-group">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#transactionModal">Add Transaction</button>
<!-- Modal -->
<div class="modal fade" id="transactionModal" tabindex="-1" aria-labelledby="transactionModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Add Transaction</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="form-group mb-3">
<label for="txn_name_modal" class="form-label">Transaction name:</label>
<input id="txn_name_modal" type="text" class="form-control">
</div>
<div class="form-group mb-3">
<label for="txn_price_modal" class="form-label">Price ($):</label>
<input id="txn_price_modal" type="numeric" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Discard Transaction</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" v-on:click="saveTransaction()">Save Transaction</button>
</div>
</div>
</div>
</div>
</div>
...
</form>
...
ユーザーが新しいトランザクションを追加するためのボタンが表示されます。 トランザクションの追加ボタンをクリックすると、トランザクションの詳細を入力するためのモーダルがユーザーに表示されます。 トランザクションの保存ボタンをクリックすると、メソッドが既存のトランザクションに追加します。
...
<form @submit.prevent="onSubmit">
...
<div class="col-md-12">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Transaction Name</th>
<th scope="col">Price ($)</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<template v-for="txn in transactions">
<tr :key="txn.id">
<th>{{ txn.id }}</th>
<td>{{ txn.name }}</td>
<td>{{ txn.price }} </td>
<td><button type="button" class="btn btn-danger" v-on:click="deleteTransaction(txn.id)">Delete</button></td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="form-group">
<button class="btn btn-primary">Create Invoice</button>
{{ loading }}
{{ status }}
</div>
</form>
...
既存のトランザクションは表形式で表示されます。 削除ボタンをクリックすると、対象のトランザクションがトランザクションリストから削除され、Invoice Price
が再計算されます。 最後に、Create Invoice
ボタンは関数をトリガーし、データを準備して、請求書を作成するためにバックエンドサーバーに送信します。
Create Invoice
コンポーネントのコンポーネント構造も見てみましょう。
...
<script>
import axios from "axios";
export default {
name: "CreateInvoice",
data() {
return {
invoice: {
name: "",
total_price: 0
},
transactions: [],
nextTxnId: 1,
loading: "",
status: ""
};
},
methods: {
...
}
};
</script>
まず、コンポーネントのデータプロパティを定義しました。 コンポーネントには、請求書name
およびtotal_price
を含む請求書オブジェクトが含まれます。 また、nextTxnId
インデックスを持つtransactions
の配列もあります。 これにより、トランザクションと変数が追跡され、ステータスの更新がユーザーに送信されます。
...
methods: {
saveTransaction() {
// append data to the arrays
let name = document.getElementById("txn_name_modal").value;
let price = document.getElementById("txn_price_modal").value;
if (name.length != 0 && price > 0) {
this.transactions.push({
id: this.nextTxnId,
name: name,
price: price
});
this.nextTxnId++;
this.calcTotal();
// clear their values
document.getElementById("txn_name_modal").value = "";
document.getElementById("txn_price_modal").value = "";
}
},
...
}
...
CreateInvoice
コンポーネントのメソッドもここで定義されています。 saveTransaction()
メソッドは、トランザクションフォームモーダルの値を取得し、それらをトランザクションリストに追加します。 deleteTransaction()
メソッドは、トランザクションのリストから既存のトランザクションオブジェクトを削除しますが、calcTotal()
メソッドは、新しいトランザクションが追加または削除されたときに合計請求価格を再計算します。
...
methods: {
...
deleteTransaction(id) {
let newList = this.transactions.filter(function(el) {
return el.id !== id;
});
this.nextTxnId--;
this.transactions = newList;
this.calcTotal();
},
calcTotal() {
let total = 0;
this.transactions.forEach(element => {
total += parseInt(element.price, 10);
});
this.invoice.total_price = total;
},
...
}
...
最後に、onSubmit()
メソッドはフォームをバックエンドサーバーに送信します。 この方法では、formData
とaxios
を使用してリクエストを送信します。 トランザクションオブジェクトを含むトランザクション配列は、2つの異なる配列に分割されます。 1つの配列はトランザクション名を保持し、もう1つの配列はトランザクション価格を保持します。 次に、サーバーは要求を処理し、ユーザーに応答を返送しようとします。
...
methods: {
...
onSubmit() {
const formData = new FormData();
this.transactions.forEach(element => {
formData.append("txn_names[]", element.name);
formData.append("txn_prices[]", element.price)
});
formData.append("name", this.invoice.name);
formData.append("user_id", this.$route.params.user.id);
this.loading = "Creating Invoice, please wait ...";
// Post to server
axios.post("http://localhost:3128/invoice", formData).then(res => {
// Post a status message
this.loading = "";
if (res.data.status == true) {
this.status = res.data.message;
} else {
this.status = res.data.message;
}
});
}
}
...
localhost:8080
でアプリケーションに戻ってサインインすると、ダッシュボードにリダイレクトされます。
ViewInvoiceコンポーネントの作成
請求書を作成できるようになったので、次のステップは、請求書とそのステータスの視覚的な画像を作成することです。 これを行うには、アプリケーションのsrc/components
ディレクトリにViewInvoices.vue
ファイルを作成します。
次のようにファイルを編集します。
<template>
<div>
<div class="tab-pane p-3 fade show active">
<div class="row">
<div class="col-md-12">
<h3>Here is a list of your invoices</h3>
<table class="table">
<thead>
<tr>
<th scope="col">Invoice #</th>
<th scope="col">Invoice Name</th>
<th scope="col">Status</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<template v-for="invoice in invoices">
<tr :key="invoice.id">
<th scope="row">{{ invoice.id }}</th>
<td>{{ invoice.name }}</td>
<td v-if="invoice.paid == 0">Unpaid</td>
<td v-else>Paid</td>
<td><a href="#" class="btn btn-success">To Invoice</a></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
...
上記のテンプレートには、ユーザーが作成した請求書を表示するテーブルが含まれています。 また、請求書がクリックされたときにユーザーを単一の請求書ページに移動させるボタンもあります。
...
<script>
import axios from "axios";
export default {
name: "ViewInvoices",
data() {
return {
invoices: [],
user: this.$route.params.user
};
},
mounted() {
axios
.get(`http://localhost:3128/invoice/user/${this.user.id}`)
.then(res => {
if (res.data.status == true) {
this.invoices = res.data.invoices;
}
});
}
};
</script>
ViewInvoices
コンポーネントには、請求書とユーザーの詳細の配列としてのデータプロパティがあります。 ユーザーの詳細は、ルートパラメータから取得されます。 コンポーネントがmounted
の場合、GET
リクエストがバックエンドサーバーに対して行われ、ユーザーが作成した請求書のリストを取得します。このリストは、前に示したテンプレートを使用して表示されます。
/dashboard
に移動したら、Navigation
の請求書の表示オプションをクリックして、請求書と支払い状況のリストを表示します。
結論
シリーズのこのパートでは、Vueの概念を使用して、請求アプリケーションのユーザーインターフェイスを構成しました。
Vueとノードを使用して軽量の請求書作成アプリを構築する方法:JWT認証と請求書の送信で学習を続けてください。