React มือใหม่ part 3: Login Example
รับประทานทฤษฎีจนท้องจะแตกแล้ว เพื่อให้มองเห็นเป็นรูปธรรมมากขึ้นเราจะเริ่มต้นด้วยโปรเจกต์ Login ง่ายๆแบบค่อยเป็นค่อยไปกันนะ
เนื้อหาต้องอาศัยความเข้าใจเรื่อง state เบื้องต้นเล็กน้อย หากมือใหม่ยังไม่รู้จักพวกมันสามารถอ่านได้ที่นี่
เนื้อหาประกอบด้วย
- create login form
- email and password fields are required
- validate email and error messages
- onClick and onBlur event handlers
Create Login Form
เราจะมาเขียน login form สำหรับใส่ email, password และปุ่ม login โดยมีเงื่อนไขย่อยคือ
- email ให้ required หมายความว่าไม่ใส่ไม่ได้และจะต้องถูกต้องตามรูปแบบของอีเมลที่กำหนด
- password ให้ required และมีความยาวมากกว่า 4 ตัวอักษร
สร้างโปรเจกต์ใหม่ชื่อ login-form
create-react-app login-form --template typescript
สร้างไฟล์ชื่อ src/Login.tsx
import React from 'react'export default class Login extends React.Component {
render() {
return <div>Login</div>
}
}
แก้ไข src/App.tsx
function App() {
return (
<div>
<Login/>
</div>
)
}
รันและดูผลลัพธ์
โปรดสังเกตว่า React กำหนดให้ฟังก์ชัน return ของ component ทำงานกับ element ได้เพียงหนึ่งเดียวเท่านั้น นี่จึงเป็นสาเหตุที่มักจะเห็น
div
element ทำหน้าที่เป็น root element ครอบ element อื่นๆภายใน component นั้น
นำ email, password และปุ่ม login มาเพิ่ม
แก้ไข src/Login.tsx ฟังก์ชัน render ให้ return JSX เหล่านี้
return <div>
<div>
<div>Email</div>
<div><input type="email" /></div>
</div>
<div>
<div>Password</div>
<div><input type="password" /></div>
</div>
<div>
<button>Login</button>
</div>
</div>
ผลลัพธ์
เพิ่ม CSS สร้าง src/Login.css
.form {
display: flex;
flex-direction: column;
width: 320px;
background-color: whitesmoke;
}
.field {
display: flex;
padding: 8px 0;
font-size: large;
}
.field > div { width: 100%; }
.field > div:first-child { text-align: left; }
input { font-size: large; }
button {
width: 100%;
font-size: large;
}
แก้ไข src/Login.tsx เพิ่ม import CSS และ CSS class
import React from ‘react’
import ‘./Login.css’export default class Login extends React.Component {
render() {
return <div className="form">
<div className="field">
<div>Email</div>
<div><input type="email" /></div>
</div>
<div className="field">
<div>Password</div>
<div><input type="password" /></div>
</div>
<div className="field">
<button>Login</button>
</div>
</div>
}
}
ผลลัพธ์
Email and Password fields are Required
input element ของ email และ password สามารถเพิ่ม required ได้เลย
แก้ไข src/Login.tsx
<div className="field">
<div>Email</div>
<div><input type="email" required/></div>
</div>
<div className="field">
<div>Password</div>
<div><input type="password" required/></div>
</div>
จากนั้นเพิ่มฟังก์ชัน handleSubmit ทำงานก็ต่อเมื่อกดปุ่ม login โดยเปลี่ยน div
แรกสุดเป็น form
ด้วย (รูปแบบมาตราฐาน) อ้างอิง
import React, { FormEvent } from 'react'...handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
}render() {
return <form className="form" onSubmit={this.handleSubmit}>
<div className="field">
<div>Email</div>
<div><input type="email" required /></div>
</div>
<div className="field">
<div>Password</div>
<div><input type="password" required /></div>
</div>
<div className="field">
<button>Login</button>
</div>
</form>
}
ผลลัพธ์เมื่อไม่กรอกอีเมล
งั้นกรอกอีเมลให้ถูกต้อง
ผลลัพธ์เมื่อไม่กรอกรหัสผ่าน
จากโค้ดข้างต้นนี้มือใหม่จะเห็นว่า onSubmit={this.handleSubmit}
ที่อยู่กับ form
สามารถเข้าใจการกดปุ่ม login ได้เอง นั่นแสดงว่า React ยังมีระบบจัดการ events เป็นของตัวเอง นอกจากนี้การกำหนดไทป์ (type) ที่เจาะจงลงไปก็ช่วยลดความผิดพลาดและการคาดเดาได้เป็นอย่างดี เช่น หากต้องการเขียน onClick ที่เกิดจาก mouse click ก็ได้ว่า
import React, { MouseEvent } from 'react'...handleSomething(event: MouseEvent<HTMLButtonElement>) {
alert('clicked')
}
Validate Email and Error Messages
email จะต้องถูกต้องตามรูปแบบของอีเมลที่กำหนด ด้วยความสามารถของ type="email"
ผลลัพธ์ที่ได้ก็น่าพอใจแล้ว
แต่เราต้องการ error message สีแดงที่บ่งบอกได้อย่างชัดเจนว่าผู้ใช้งานทำรายการไม่ถูกต้อง เรื่องนี้จะยากขึ้นอีกหน่อย
แล้วทำอย่างไรล่ะ?
อย่างแรกให้เรามองว่าหนึ่งฟิลด์ใดๆคือออบเจ็กต์ โดยออบเจ็กต์มีคุณลักษณะเป็นของมันเอง ขอยกฟิลด์ email มาเป็นตัวอย่าง
email ประกอบด้วย
- ค่าคือ value เช่น pros@hotmail.com
- ความถูกต้องคือ valid เช่น กรอกข้อมูลถูกต้องตาม format ที่กำหนด
- ข้อความความผิดพลาดคือ errorMsg เช่น โปรดระบุรูปแบบอีเมลที่ถูกต้อง
ประกอบร่าง value, valid และ errorMsg ขอตั้งชื่อว่า FieldState หน้าตาก็จะเป็นแบบนี้ครับ
interface FieldState {
value: string
valid: boolean
errorMsg: string
}
เพิ่ม constructor แล้วกำหนด FieldState นี้ให้กับ email และ password ในออบเจ็กต์ state แน่นอนว่า TypeScript บังคับให้เรากำหนดออบเจ็กต์ props ด้วยซึ่งเราไม่ต้องการเลย ฉะนั้นละไว้เป็นออบเจ็กต์ว่าง
constructor(props: {}) {
super(props)
this.state = {
email: { value: '', valid: true, errorMsg: '' },
password: { value: '', valid: true, errorMsg: '' },
}
}
นิยามชนิดของ state
interface LoginState {
email: FieldState,
password: FieldState,
}
และอย่าลืมบอกกับ React.Component ว่า props กับ state มีหน้าตาเป็นอย่างไร
export default class Login extends React.Component<{}, LoginState> { ... }
ไปดูที่ UI กันบ้าง
เพิ่มการอ่านค่า value จาก state ของ email
<input type="email" required value={this.state.email.value} />
เพิ่มการอ่านค่า value จาก state ของ password
<input type="password" required value={this.state.password.value} />
ผลลัพธ์
การอ่านค่าจาก state ให้กับ value โดย default จะเป็นการตีความว่าค่านั้นเปลี่ยนแปลงได้ ดังนั้นจะขาดเหตุการณ์ onChange ไม่ได้
เพิ่ม onChange ให้กับ email
<input type="email" required value={this.state.email.value} onChange={this.handleEmail} />
เพิ่ม onChange ให้กับ password
<input type="password" required value={this.state.password.value} onChange={this.handlePassword} />
อย่าลืมเขียนฟังก์ชันตามที่ได้ตั้งชื่อนั่นด้วย
handleEmail() { }handlePassword() { }
เพื่อให้มีข้อความความผิดพลาดรูปแบบอีเมลไม่ถูกต้อง ให้เพิ่ม div
ไว้ใต้ input
ดังนี้
<div className="field">
<div>Email</div>
<div>
<input type="email" required value={this.state.email.value} onChange={this.handleEmail} />
<div className="error">โปรดระบุรูปแบบอีเมลให้ถูกต้อง</div>
</div>
</div>
CSS
....error {
color: red;
}
ผลลัพธ์
โดยที่ CSS ของคลาส error ได้กำหนดใช้อักษรเป็นสีแดง
แต่ประเด็นคือข้อความความผิดพลาดต้องปรากฏหลังจากเราพิมพ์ค่าบางส่วนลงไปและค่านั้นไม่ถูกต้องตามรูปแบบอีเมลที่ควรจะเป็น หมายความว่าต้องมี logic (ความคิด) เพิ่มเข้าไปอีก
เพิ่มการตรวจสอบว่า email นี้ valid (รูปแบบถูกต้อง) แล้วหรือไม่
<div className="field">
<div>Email</div>
<div>
<input type="email" required value={this.state.email.value} onChange={this.handleEmail} />
{!this.state.email.valid && <div className="error">โปรดระบุรูปแบบอีเมลให้ถูกต้อง</div>}
</div>
</div>
โค้ดข้างต้น this.state.email.valid
จะยังให้ค่าเป็น true เสมอในตอนนี้ (เพราะเรากำหนดให้มันเป็น true ตั้งแต่แรก) เมื่อใส่เครื่องหมาย ! ก็จะให้ผลลัพธ์เป็นตรงข้ามคือ false เสมอในตอนนี้
เครื่องหมาย && จะมีผลให้ <div className="error">โปรดระบุรูปแบบอีเมลให้ถูกต้อง</div>
แสดงขึ้นมาก็ต่อเมื่อเงื่อนไขด้านซ้ายของมันเป็น true เท่านั้น อ้างอิง
นั่นหมายความว่า this.state.email.valid
มีค่าเป็น true ก็ไม่ต้องการแสดงข้อความความผิดพลาด ตรงกันข้ามถ้า this.state.email.valid
มีค่าเป็น false ให้แสดงข้อความความผิดพลาดออกมา
ผลลัพธ์
หวังว่าจะตามผมทันนะครับ
กลับไปที่ฟังก์ชัน handleEmail ลอง console.log ดูค่าที่เราพิมพ์เข้าไป
handleEmail(event: FormEvent<HTMLInputElement>) {
const value = event.currentTarget.value
console.log(value)
}
คำถามคือทำไมมันไม่พิมพ์ลงไปในช่อง input
คำตอบคือค่าที่เราพิมพ์ไม่ถูกกำหนดลง state หรือพูดได้ว่า state ไม่รู้ว่าเกิดการเปลี่ยนแปลง (change)
งั้นก็ต้องทำให้มันรู้ด้วยการปั้นออบเจ็กต์ state ตัวใหม่ที่จะยึดโยงกับ state ตัวเก่าหรือไม่ก็ได้ แต่ตัวอย่างนี้ยึดโยงกับ state ตัวเก่าด้วย
ยังจำ part 2 ได้ไหม เราจะเรียก this.setState
แล้วใส่ฟังก์ชันลงไป โดยฟังก์ชันนี้ให้ชื่อว่า newState มันจะคืนค่าของออบเจ็กต์ state ตัวใหม่
handleEmail(event: FormEvent<HTMLInputElement>) {
const value = event.currentTarget.value
console.log(value)
const newState = (state: LoginState) => {
return {
...state,
email: {
value,
valid: true,
errorMsg: '',
}
}
}
this.setState(newState)
}
หรือเขียนให้น้อยลงได้แบบนี้
handleEmail(event: FormEvent<HTMLInputElement>) {
const value = event.currentTarget.value
console.log(value)
this.setState((state) => {
return {
...state,
email: {
value,
valid: true,
errorMsg: '',
}
}
})
}
และอย่าลืมผูกฟังก์ชันที่อ้างอิง state ให้เป็นของคลาส โดยบ่งบอกใน constructor
constructor(props: {}) {
super(props)
this.state = {
email: { value: '', valid: true, errorMsg: '' },
password: { value: '', valid: true, errorMsg: '' },
}
this.handleEmail = this.handleEmail.bind(this)
}
ผลลัพธ์
มือใหม่ที่ไม่คุ้นเคยภาษา JavaScript มาตราฐานใหม่อย่าง ES6 เชื่อว่างงทุกราย เช่น
ถ้าตัวแปรชื่อ value แล้วต้องการให้ค่ากับตัวแปร x.value ยังไงก็ต้องเขียนแบบนี้
const value = 100
x.value = value
แต่ถ้าเขียนในรูปแบบของออบเจ็กต์ x ที่มีสมาชิกเป็น value จะได้
const value = 100
const x = { value: value }
ES6 เห็นแบบนี้จึงกล่าว จะเขียน value ให้ value ซ้ำซากทำไม เอาเท่านี้พอ
const value = 100
const x = { value }
พอเขียนเป็นค่าของ return ก็เท่านี้พอ
const value = 100
return {
value
}
อีกตัวอย่าง สมมติมีตัวแปร x = { w: 1, h: 2}
ต้องการให้ค่าแก่ตัวแปร y ได้ว่า
const x = { w:1, h:2 }
const y = { ...x }
ผลคือคัดลอก w ที่มีค่าเท่ากับ 1 และ h ที่มีค่าเท่ากับ 2 ให้กับออบเจ็กต์ y
แล้วถ้าต้องการเปลี่ยนเฉพาะค่า w ของออบเจ็กต์ y ให้เป็น 3 ล่ะ? ES6 บอกว่าทำแบบนี้ได้นะ
const x = { w:1, h:2 }
const y = { ...x, w: 3 }
ผลคือมันจะคัดลอก w ที่มีค่าเท่ากับ 1 และ h ที่มีค่าเท่ากับ 2 ให้กับออบเจ็กต์ y ก่อน จากนั้นให้ค่า w ใหม่ (เดิม 1 เปลี่ยนเป็น 3)
เพิ่มการจัดการ validation ในฟังก์ชัน handleEmail
handleEmail(event: FormEvent<HTMLInputElement>) {
const value = event.currentTarget.value
const valid = this.validateEmail(value)
this.setState((state) => {
return {
...state,
email: {
value,
valid,
errorMsg: valid? '': 'โปรดระบุรูปแบบอีเมลให้ถูกต้อง'
}
}
})
}
ฟังก์ชัน validateEmail ซึ่งให้ผลลัพธ์เป็น boolean
validateEmail(value: string): boolean {
return !!value.match(/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/)
}
เครื่องหมาย !! ใช้เปลี่ยนค่าของ RegExpMatchArray หรือ null เป็น boolean
จากนั้นเปลี่ยนข้อความความผิดพลาดใน JSX ให้เป็น this.state.email.errorMsg
<div className="field">
<div>Email</div>
<div>
<input type="email" required value={this.state.email.value} onChange={this.handleEmail} />
{!this.state.email.valid && <div className="error">{this.state.email.errorMsg}</div>}
</div>
</div>
ผลลัพธ์
โค้ด login form — validate email ทั้งหมดในตอนนี้
ข้อสังเกตุ ตามที่ผมได้ออกแบบ errorMsg ไว้ สามารถเลือกได้ 2 แบบ
- กำหนดข้อความความผิดพลาดที่ต้องการให้กับ errorMsg จากนั้นจึงส่งเข้า setState เหมือนกับโค้ดล่าสุดนี้
- หรือไม่ต้องมี errorMsg ก็ได้ แต่ให้เขียนข้อความความผิดพลาดที่ต้องการใน JSX อย่างเดิม
หลักพิจารณาว่าจะเลือกแบบไหนสำหรับผมคือ errorMsg สามารถเปลี่ยนได้หลายค่าไหม ถ้ามีแค่สตริงว่างกับ ‘โปรดระบุรูปแบบอีเมลให้ถูกต้อง’ ถ้าแค่นี้ก็ไม่จำเป็นต้องมี errorMsg เท่ากับว่า FieldState interface ตัด errorMsg ออกได้เลย
Validate Password and Error Messages
เริ่มด้วยการตัด errorMsg ออกแล้วเขียนข้อความความผิดพลาดใน JSX แทน
JSX ของ email
<div className="field">
<div>Email</div>
<div>
<input type="email" required value={this.state.email.value} onChange={this.handleEmail} />
{!this.state.email.valid && <div className="error">โปรดระบุรูปแบบอีเมลให้ถูกต้อง</div>}
</div>
</div>
JSX ของ password
<div className="field">
<div>Password</div>
<div>
<input type="password" required value={this.state.password.value} onChange={this.handlePassword} />
{!this.state.password.valid && <div className="error">รหัสผ่านต้องมีความยาวไม่น้อยกว่าสี่ตัวอักษร</div>}
</div>
</div>
พอเข้าใจและทำได้คล่องแล้วใช่ไหม ต่อเลยครับ
interface FieldState ตัด errorMsg ออก
interface FieldState {
value: string
valid: boolean
}
ออบเจ็กต์ state ของการกำหนดค่าเริ่มต้น ตัด errorMsg ออก
this.state = {
email: { value: '', valid: true, },
password: { value: '', valid: true, },
}
ฟังก์ชัน handleEmail ส่วนของ setState ตัด errorMsg ออก
handleEmail(event: FormEvent<HTMLInputElement>) {
const value = event.currentTarget.value
const valid = this.validateEmail(value)
this.setState((state) => {
return {
...state,
email: {
value,
valid,
}
}
})
}
ฟังก์ชัน handlePassword เพิ่มการตรวจสอบและ setState
handlePassword(event: FormEvent<HTMLInputElement>) {
const value = event.currentTarget.value
const valid = this.validatePassword(value)
this.setState((state)=> {
return {
...state,
password: {
value,
valid,
}
}
})
}
ฟังก์ชัน validatePassword
validatePassword(value: string): boolean {
return value.length >= 4
}
อย่าลืมไปผูกฟังก์ชันนี้กับคลาส Login นี้ใน constructor ของมัน
this.handlePassword = this.handlePassword.bind(this)
ผลลัพธ์
onClick and onBlur event handlers
มือใหม่ได้รู้จัก onSubmit ของ form
และ onChange ของ input
หัวข้อนี้ขอแนะนำ onClick และ onBlur ให้ด้วย
เราสามารถออกแบบสถานการณ์การแจ้งข้อความความผิดพลาดได้หลากหลาย อีกแบบที่ขอแนะนำคือการใช้ onBlur ร่วมกับ onChange
JSX ของ email
<div className="field">
<div>Email</div>
<div>
<input type="email" required value={this.state.email.value} onChange={this.handleEmail} onBlur={this.handleValidEmail} />
{!this.state.email.valid && <div className="error">โปรดระบุรูปแบบอีเมลให้ถูกต้อง</div>}
</div>
</div>
ฟังก์ชัน handleValidEmail จะทำงานก็ต่อเมื่อ loses focus เราอาจใช้มันเรียก APIs ทำ callback function หรือสิ่งอื่นได้
import React, { FormEvent, FocusEvent } from 'react'...handleValidEmail(event: FocusEvent<HTMLInputElement>) {
if(this.state.email.value.length > 0 && this.state.email.valid) {
alert('call apis or do something after loses focus')
}
}
เงื่อนไขนี้คือจนกว่าจะมีการพิมพ์อีเมลและรูปแบบอีเมลถูกต้องจึงจะทำงาน alert
อย่าลืมผูกฟังก์ชันที่ constructor
this.handleValidEmail = this.handleValidEmail.bind(this)
ผลลัพธ์
อย่างสุดท้ายที่เชื่อว่าหลายคนคงใช้เป็นคือ onClick
ผมจะเพิ่ม forgot password แบบลิงก์ลืมรหัสผ่าน, React แนะนำว่าหากใช้ a
และระบุ href
เป็น # ให้เปลี่ยนไปใช้ button
แล้วแต่ง CSS เป็นลิงก์จะดีกว่าครับ อ้างอิง
<div className="field">
<div>Password</div>
<div>
<div>
<input type="password" required value={this.state.password.value} onChange={this.handlePassword} />
{!this.state.password.valid && <div className="error">รหัสผ่านต้องมีความยาวไม่น้อยกว่าสี่ตัวอักษร</div>}
</div>
<div className="forgot-password">
<button className="link-button" onClick={this.handleForgotPassword}>forgot password</button>
</div>
</div>
</div>
CSS
....forgot-password {
padding-top: 8px;
text-align: end;
}
.link-button {
color: gray;
font-size: small;
display: inline;
border: none;
background-color: transparent;
padding: 0;
text-decoration: underline;
cursor: pointer;
width: fit-content;
}
ฟังก์ชัน handleForgotPassword
import React, { FormEvent, FocusEvent, MouseEvent } from 'react'...handleForgotPassword(event: MouseEvent<HTMLButtonElement>) {
if (this.state.email.value.length > 0 && this.state.email.valid) {
alert('send reset password email')
}
}
อย่าลืม
this.handleForgotPassword = this.handleForgotPassword.bind(this)
ผลลัพธ์
โค้ดทั้งหมด
โค้ดข้างต้นนี้ผมเรียน ค้นคว้าและเขียนขึ้นมาสดๆ ผิดพลาดประการใดก็ขอให้เป็นบทเรียนเพื่อใช้ในการปรับปรุงตนในอนาคตข้างหน้า หวังว่ามือใหม่หลายท่านจะได้ประโยชน์จากตัวอย่างนี้นะครับ แล้วเจอกันใหม่