Google先生みたいなログイン画面が作りたかった。 それには「Material UI」を使えばいいので Material UIについて勉強しました
前回の続きになります
Material UIとは
GoogleのMaterialデザインをベースに開発された、UIコンポーネントライブラリです。 Google先生のサイトを使った事ある人ならすぐに受け入れてくるのがいい点です。
ログイン画面について
一般的なものです。Google先生のログインはメールアドレスとパスワードが別になっています。 自分が作りたいものは別ではなく一緒のものです。例としてははてなのログイン画面です
実際のログイン画面にはログイン処理および現在のログイン状態が必要なので、複雑です。今回はログイン画面の基礎みたいな所です。この部分はサービスを作る上では必須なのでちゃんと理解したかった。
参考ソースについて
githubに上がっているソースになります
参考にしたソースはTypeScriptでした自分も同じようにTypeScriptにした方がよかったのですが 他のソースとのレベル間があるので普通のjavascriptにしました。
ソース
クリックすると展開されます(長文なので注意)
import React, { useReducer, useEffect } from "react"; import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardActions from "@material-ui/core/CardActions"; import CardHeader from "@material-ui/core/CardHeader"; import Button from "@material-ui/core/Button"; const useStyles = makeStyles((theme: Theme) => createStyles({ container: { display: "flex", flexWrap: "wrap", width: 400, margin: `${theme.spacing(0)} auto` }, loginBtn: { marginTop: theme.spacing(2), flexGrow: 1 }, header: { textAlign: "center", background: "#212121", color: "#fff" }, card: { marginTop: theme.spacing(10) } }) ); //state type /* type State = { username: string password: string isButtonDisabled: boolean helperText: string isError: boolean }; */ const initialState: State = { username: "", password: "", isButtonDisabled: true, helperText: "", isError: false }; type Action = | { type: "setUsername", payload: string } | { type: "setPassword", payload: string } | { type: "setIsButtonDisabled", payload: boolean } | { type: "loginSuccess", payload: string } | { type: "loginFailed", payload: string } | { type: "setIsError", payload: boolean }; const reducer = (state: State, action: Action): State => { switch (action.type) { case "setUsername": return { ...state, username: action.payload }; case "setPassword": return { ...state, password: action.payload }; case "setIsButtonDisabled": return { ...state, isButtonDisabled: action.payload }; case "loginSuccess": return { ...state, helperText: action.payload, isError: false }; case "loginFailed": return { ...state, helperText: action.payload, isError: true }; case "setIsError": return { ...state, isError: action.payload }; default: return state; } }; const Login = () => { const classes = useStyles(); const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { if (state.username.trim() && state.password.trim()) { dispatch({ type: "setIsButtonDisabled", payload: false }); } else { dispatch({ type: "setIsButtonDisabled", payload: true }); } }, [state.username, state.password]); const handleLogin = () => { if (state.username === "abc@email.com" && state.password === "password") { dispatch({ type: "loginSuccess", payload: "Login Successfully" }); } else { dispatch({ type: "loginFailed", payload: "Incorrect username or password" }); } }; const handleKeyPress = (event: React.KeyboardEvent) => { if (event.keyCode === 13 || event.which === 13) { state.isButtonDisabled || handleLogin(); } }; const handleUsernameChange: React.ChangeEventHandler<HTMLInputElement> = ( event ) => { dispatch({ type: "setUsername", payload: event.target.value }); }; const handlePasswordChange: React.ChangeEventHandler<HTMLInputElement> = ( event ) => { dispatch({ type: "setPassword", payload: event.target.value }); }; return ( <form className={classes.container} noValidate autoComplete="off"> <CardHeader className={classes.header} title="Login App" /> <Card className={classes.card}> <CardContent> <div> <TextField error={state.isError} fullWidth id="username" type="email" label="Username" placeholder="Username" margin="normal" onChange={handleUsernameChange} onKeyPress={handleKeyPress} /> <TextField error={state.isError} fullWidth id="password" type="password" label="Password" placeholder="Password" margin="normal" helperText={state.helperText} onChange={handlePasswordChange} onKeyPress={handleKeyPress} /> </div> </CardContent> <CardActions> <Button variant="contained" size="large" color="secondary" className={classes.loginBtn} onClick={handleLogin} disabled={state.isButtonDisabled} > Login </Button> </CardActions> </Card> </form> ); }; export default Login;
結果
シンプルなログイン画面です
ソースの解説
長文なのでコピペしただけだと理解できていないので未来の自分用(笑)に解説します。
useReducerやuseEffectについては省略します。
正直にいうとこの部分のためにReact Hooksを勉強しましたwww
typescriptからjavascriptに変更した部分
タイプに関してはカンマをつけました 修正前
type State = { username: string password: string isButtonDisabled: boolean helperText: string isError: boolean };
修正後
type State = { username: string, password: string, isButtonDisabled: boolean, helperText: string, isError: boolean, };
TypeScriptの場合はいらないのかな・・・よくわからないです。
デザインの部分
デザイン部分に関してはMaterial UIのmakeStylesで 作成して指定しています。ここに関しては難しい所はないかと・・・
const useStyles = makeStyles((theme: Theme) => createStyles({ container: { display: "flex", flexWrap: "wrap", width: 400, margin: `${theme.spacing(0)} auto` }, loginBtn: { marginTop: theme.spacing(2), flexGrow: 1 }, header: { textAlign: "center", background: "#212121", color: "#fff" }, card: { marginTop: theme.spacing(10) } }) );
ログインのフォームのHTML部分
ログイン画面はMaterial UIのCardの機能を使って作っています
<form className={classes.container} noValidate autoComplete="off"> <Card className={classes.card}> <CardHeader className={classes.header} title="Login App" /> <CardContent> <div> <TextField error={state.isError} fullWidth id="username" type="email" label="Username" placeholder="Username" margin="normal" onChange={handleUsernameChange} onKeyPress={handleKeyPress} /> <TextField error={state.isError} fullWidth id="password" type="password" label="Password" placeholder="Password" margin="normal" helperText={state.helperText} onChange={handlePasswordChange} onKeyPress={handleKeyPress} /> </div> </CardContent> <CardActions> <Button variant="contained" size="large" color="secondary" className={classes.loginBtn} onClick={handleLogin} disabled={state.isButtonDisabled} > Login </Button> </CardActions> </Card> </form>
Cardの中に
- CardHeader
- CardContent
- CardActions
があります
CardHeader
ログイン部分のヘッダーはシンプルです
<CardHeader className={classes.header} title="Login App" />
上記以外にavatar、action、subheaderなども指定できます
<CardHeader avatar={ <Avatar aria-label="recipe" className={classes.avatar}> R </Avatar> } action={ <IconButton aria-label="settings"> <MoreVertIcon /> </IconButton> } title="Shrimp and Chorizo Paella" subheader="September 14, 2016" />
CardContent
主軸になるコンテンツを記載します。 ログイン画面なのでログインフォームの本体になります
<CardContent> <div> <TextField error={state.isError} fullWidth id="username" type="email" label="Username" placeholder="Username" margin="normal" onChange={handleUsernameChange} onKeyPress={handleKeyPress} /> <TextField error={state.isError} fullWidth id="password" type="password" label="Password" placeholder="Password" margin="normal" helperText={state.helperText} onChange={handlePasswordChange} onKeyPress={handleKeyPress} /> </div> </CardContent>
はてなのログインみたいにログインのボタンが複数になる場合は
CardContentにログインボタンを配置した方がいいです。
fullWidthでカードの横幅になるみたいなので便利!!
CardActions
カードに対してのアクションを指定します。
ボタンやアイコンやリンクなどを指定できます。またもっと見るみたいな行為もできる。
アクションは1行しか表現できないので複数行の場合はコンテンツで記載するもよう・・・
アイコンの場合
<CardActions disableSpacing> <IconButton aria-label="add to favorites"> <FavoriteIcon /> </IconButton> <IconButton aria-label="share"> <ShareIcon /> </IconButton> <IconButton className={clsx(classes.expand, { [classes.expandOpen]: expanded, })} onClick={handleExpandClick} aria-expanded={expanded} aria-label="show more" > <ExpandMoreIcon /> </IconButton> </CardActions>
リンクの場合
<CardActions> <Button size="small" color="primary"> Share </Button> <Button size="small" color="primary"> Learn More </Button> </CardActions>
ボタンの場合(リンクとボタン違いはvariant="contained")
<CardActions> <Button size="small" variant="contained" color="primary"> Share </Button> <Button size="small" variant="contained" color="primary"> Learn More </Button> </CardActions>
ログインのフォームの処理部分
4つの処理で構成されています
- 入力された値を保存する処理
- ボタンを有効化する処理
- ログイン処理
値を保存する部分はユーザー名とパスワードがありますが、ほぼ同じなので1つだけ記載します
入力された値を保存する処理
ユーザー名が入力される(onChange)とhandleUsernameChangeの関数が起動します。
const handleUsernameChange: React.ChangeEventHandler<HTMLInputElement> = ( event ) => { dispatch({ type: "setUsername", payload: event.target.value }); };
その値をuseReducerで作成したdispatchの関数に渡します。 dispatchの関数はreducerという関数をもとに作っています
const [state, dispatch] = useReducer(reducer, initialState);
そのreducerは状態遷移の関数です
const reducer = (state: State, action: Action): State => { switch (action.type) { case "setUsername": return { ...state, username: action.payload }; case "setPassword": return { ...state, password: action.payload }; case "setIsButtonDisabled": return { ...state, isButtonDisabled: action.payload }; case "loginSuccess": return { ...state, helperText: action.payload, isError: false }; case "loginFailed": return { ...state, helperText: action.payload, isError: true }; case "setIsError": return { ...state, isError: action.payload }; default: return state; } };
typeがsetUsernameなので
case "setUsername": return { ...state, username: action.payload };
渡ってきたstateのusernameにaction.payloadを上書きしています。 そのほかの値は変更しません。
ちなみに「: React.ChangeEventHandler
const handleUsernameChange = ( event ) => { dispatch({ type: "setUsername", payload: event.target.value }); };
ボタンを有効化する処理
ユーザー名とパスワード名が入力されるとonChangeが起動されてhandleUsernameChangeまたはhandlePasswordChangeのonChangeが起動されます。 起動されるとstate.usernameとstate.passwordの値が変更されます。 変更されるとuseEffectが起動されます。
useEffect(() => { if (state.username.trim() && state.password.trim()) { dispatch({ type: "setIsButtonDisabled", payload: false }); } else { dispatch({ type: "setIsButtonDisabled", payload: true }); } console.log("useEffect"); }, [state.username, state.password]);
どちらも入力されていた場合にdispatchが起動される
dispatch({ type: "setIsButtonDisabled", payload: false });
reducerのsetIsButtonDisabledが起動されてボタンの有効化、無効化を変化させている
case "setIsButtonDisabled": return { ...state, isButtonDisabled: action.payload };
ログイン処理
ログインのボタンが押されるとhandleLoginが起動される。 この部分は本来は認証させる部分なので本当はもっと複雑。
const handleLogin = () => { if (state.username === "abc@email.com" && state.password === "password") { dispatch({ type: "loginSuccess", payload: "Login Successfully" }); } else { dispatch({ type: "loginFailed", payload: "Incorrect username or password" }); } };
ログインが成功するとloginSuccessのdispatchが起動されて画面にも反映される。 失敗するとloginFailedが起動される。
ちなみにユーザー名はパスワードをTextFieldにいる状態でEnterキーを入力するとonKeyPressで設定されているhandleKeyPressが起動される。 (その他のキーでも起動するのでが・・・)
const handleKeyPress = (event: React.KeyboardEvent) => { if (event.keyCode === 13 || event.which === 13) { console.log("enter key"); state.isButtonDisabled || handleLogin(); } };
Enterキーのコードの13だった場合にログインの関数のhandleLoginを起動しています。
このEnterキーの処理はいるのかと聞かれると・・・といわれる・・・う~ん、他のサイトでもやっているみたい。
必須はないが、あるとUI的にはいいっぽい。
感想
これでログイン画面の基礎ができた。 FireBaseにログインさせる場合でも普通の認証でも使うのでよかったと思う。 Cardの部分についても勉強できたのでよかった