[前回] Web3.0検証(16)-MeteorでTODO管理アプリの開発(タスク絞込み機能とUI改善)
はじめに
TODO管理アプリに、ユーザーアカウント機能を追加します。
ログイン画面やパスワード認証も実装します。
準備
-
端末を二つ用意します。
- 端末1: インストール/起動/確認作業
- 端末2: ソースコード改編
-
端末1から、Meteorアプリを実行
$ cd simple-todos-react
$ meteor run
- ブラウザでアプリを確認
-
http://localhost:3000/
にアクセス - デベロッパーツールを開く(F12)
-
デバイスのツールバーを切り替え
アイコンをクリック - デバイスを選択
-
パスワード認証によるユーザーログイン
-
作業内容
- Meteorには、基本認証およびアカウント管理システムが内蔵されている
-
accounts-password
を追加するだけで、ユーザー名とパスワードによる認証を使用できる(すごい) - Facebook、Google、GitHubなどを使用したログイン認証も可能
-
端末1から、必要モジュールをインストール
$ meteor add accounts-password
$ meteor add accounts-base
$ meteor npm install --save bcrypt
- ※
npm
でなくmeteor npm
を使用することで、常に同じnpmバージョンを担保- 異なるnpmバージョンを用いてインストールされた、モジュール不一致による予想外問題を回避
ユーザーアカウントを作成
-
作業内容
- アプリのデフォルトユーザーを作成
- ユーザー名:
meteorite
- パスワード:
password
- ユーザー名:
- データベースにユーザーが存在するかチェックし
- 見つからなかった場合、新しいユーザーを作成
- アプリのデフォルトユーザーを作成
-
端末2から、コード修正
$ cd simple-todos-react
$ vi server/main.js
server/main.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TasksCollection } from '/imports/api/TasksCollection';
... ...
const SEED_USERNAME = 'meteorite';
const SEED_PASSWORD = 'password';
Meteor.startup(() => {
if (!Accounts.findUserByUsername(SEED_USERNAME)) {
Accounts.createUser({
username: SEED_USERNAME,
password: SEED_PASSWORD,
});
}
... ...
});
ログインフォーム
-
作業内容
- フォームを作成し、ユーザーが認証情報を入力可能に
-
useState
フックを使用し実装 -
LoginForm.jsx
という名前の新しいファイルを作成し、フォーム追加 -
Meteor.loginWithPassword(username、password);
関数を使用し認証を行う
-
コード
imports/ui/LoginForm.jsx
import { Meteor } from 'meteor/meteor';
import React, { useState } from 'react';
export const LoginForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const submit = e => {
e.preventDefault();
Meteor.loginWithPassword(username, password);
};
return (
<form onSubmit={submit} className="login-form">
<label htmlFor="username">Username</label>
<input
type="text"
placeholder="Username"
name="username"
required
onChange={e => setUsername(e.target.value)}
/>
<label htmlFor="password">Password</label>
<input
type="password"
placeholder="Password"
name="password"
required
onChange={e => setPassword(e.target.value)}
/>
<button type="submit">Log In</button>
</form>
);
};
認証処理
-
作業内容
- 認証されたユーザーのみ、タスク管理機能にアクセスできるようにする
- 認証ユーザーが存在しない場合
-
LoginForm
コンポーネントを返す
-
- 認証ユーザーが存在する場合
- 3つのコンポーネント、
form
、filter
、list
を<Fragment>
でラップしてから返す - Fragmentとは
- Reactで複数コンポーネントをrender()関数で返す場合、1つのタグにまとめる必要あり
- グループ化のため他の余分なタグを使用すると、不本意にUI(DOM)に影響する恐れあり
- Fragmentタグを使用することで、複数コンポーネントをグループ化可能、かつUIに影響しない
- 3つのコンポーネント、
- 認証されたユーザーの取得
-
Meteor.user()
をuseTracker
フックでラップし、リアクティブ化
-
-
コード
imports/ui/App.jsx
import { Meteor } from 'meteor/meteor';
import React, { useState, Fragment } from 'react';
import { useTracker } from 'meteor/react-meteor-data';
import { TasksCollection } from '/imports/api/TasksCollection';
import { Task } from './Task';
import { TaskForm } from './TaskForm';
import { LoginForm } from './LoginForm';
... ...
export const App = () => {
const user = useTracker(() => Meteor.user());
... ...
return (
... ...
<div className="main">
{user ? (
<Fragment>
<TaskForm />
<div className="filter">
<button onClick={() => setHideCompleted(!hideCompleted)}>
{hideCompleted ? 'Show All' : 'Hide Completed'}
</button>
</div>
<ul className="tasks">
{tasks.map(task => (
<Task
key={task._id}
task={task}
onCheckboxClick={toggleChecked}
onDeleteClick={deleteTask}
/>
))}
</ul>
</Fragment>
) : (
<LoginForm />
)}
</div>
... ...
ログインフォームのスタイル
-
作業内容
- ログインフォームのスタイルを設定
-
label
、input
、button
などのタグをdiv
でラップ- CSSで制御しやすくする
-
- ログインフォームのスタイルを設定
-
コード
imports/ui/LoginForm.jsx
<form onSubmit={submit} className="login-form">
<div>
<label htmlFor="username">Username</label>
<input
type="text"
placeholder="Username"
name="username"
required
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
placeholder="Password"
name="password"
required
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<button type="submit">Log In</button>
</div>
</form>
- CSSを更新
client/main.css
.login-form {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
align-items: center;
}
.login-form > div {
margin: 8px;
}
.login-form > div > label {
font-weight: bold;
}
.login-form > div > input {
flex-grow: 1;
box-sizing: border-box;
padding: 10px 6px;
background: transparent;
border: 1px solid #aaa;
width: 100%;
font-size: 1em;
margin-right: 16px;
margin-top: 4px;
}
.login-form > div > input:focus {
outline: 0;
}
.login-form > div > button {
background-color: #62807e;
}
これで、ログインフォームが一元化され、かっこよく見えます。
サーバーの起動
-
作業内容
- すべてのタスクに所有者を付与
-
db.tasks.remove({});
を実行し、データベースのすべてのタスクを削除 -
server/main.js
を変更し、meteorite
ユーザーを所有者とするシードタスクを追加- insertされるレコードのフィールド
-
text
: タスク内容 -
userId
: userの_id
値 -
createdAt
: 日付
-
- insertされるレコードのフィールド
- 変更後、meteorを再起動
-
Meteor.startup
ブロックを再実行するため
-
-
コード
server/main.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TasksCollection } from '/imports/api/TasksCollection';
const insertTask = (taskText, user) =>
TasksCollection.insert({
text: taskText,
userId: user._id,
createdAt: new Date(),
});
const SEED_USERNAME = 'meteorite';
const SEED_PASSWORD = 'password';
Meteor.startup(() => {
if (!Accounts.findUserByUsername(SEED_USERNAME)) {
Accounts.createUser({
username: SEED_USERNAME,
password: SEED_PASSWORD,
});
}
const user = Accounts.findUserByUsername(SEED_USERNAME);
if (TasksCollection.find().count() === 0) {
[
'First Task',
'Second Task',
'Third Task',
'Fourth Task',
'Fifth Task',
'Sixth Task',
'Seventh Task',
].forEach(taskText => insertTask(taskText, user));
}
});
所有者によるタスクの絞込み
-
作業内容
- ログイン済みユーザーが所有するタスクのみ絞り込む
- Mini Mongoからタスク取得時、userの
_id
値を持つuserId
フィールドをMongoセレクターに追加
- Mini Mongoからタスク取得時、userの
- ログイン済みユーザーが所有するタスクのみ絞り込む
-
コード
imports/ui/App.jsx
... ...
const hideCompletedFilter = { isChecked: { $ne: true } };
const userFilter = user ? { userId: user._id } : {};
const pendingOnlyFilter = { ...hideCompletedFilter, ...userFilter };
const tasks = useTracker(() => {
if (!user) {
return [];
}
return TasksCollection.find(
hideCompleted ? pendingOnlyFilter : userFilter,
{
sort: { createdAt: -1 },
}
).fetch();
});
const pendingTasksCount = useTracker(() => {
if (!user) {
return 0;
}
return TasksCollection.find(pendingOnlyFilter).count();
});
... ...
<TaskForm user={user} />
... ...
- insert処理を更新し、Taskフォームに
userId
フィールドを含める- Appコンポーネントから
TaskForm
にユーザーを渡す必要あり
- Appコンポーネントから
- コード
imports/ui/TaskForm.jsx
... ...
export const TaskForm = ({ user }) => {
const [text, setText] = useState('');
const handleSubmit = e => {
e.preventDefault();
if (!text) return;
TasksCollection.insert({
text: text.trim(),
createdAt: new Date(),
userId: user._id
});
setText('');
};
... ...
ログアウト
-
作業内容
- アプリバーの下に所有者のユーザー名を表示
- タスクをまとめて表示するため、
Fragment
開始タグの直後に新しいdivを含める -
onClick
ハンドラーを追加し、Meteor.logout()を呼び出すことでログアウト
-
コード
imports/ui/App.jsx
... ...
const logout = () => Meteor.logout();
return (
... ...
<Fragment>
<div className="user" onClick={logout}>
{user.username} 🚪
</div>
... ...
- ユーザー名のスタイル設定
client/main.css
.user {
display: flex;
align-self: flex-end;
margin: 8px 16px 0;
font-weight: bold;
}
ブラウザでアプリを確認
ユーザー名とパスワードを入力し、ログインします。
- ユーザー名:
meteorite
- パスワード:
password
ログイン前 | ログイン後 |
---|---|
認証成功し、タスク一覧が表示されました。やったー。
おわりに
今回は、ユーザーアカウントと認証機能を追加しました。
ユーザーとタスクの関連付けも行いました。
次回も続きます。お楽しみに。