اصول کلی طراحی یک اعتبارسنج ساده
در قسمت قبل، تمام اطلاعات فرم لاگین را درون شیء account خاصیت state قرار دادیم. در اینجا نیز شبیه به چنین شیءای را برای ذخیره سازی خطاهای اعتبارسنجی فیلدهای فرم، تعریف میکنیم:
class LoginForm extends Component { state = { account: { username: "", password: "" }, errors: { username: "Username is required" } };
البته در ابتدای کار، خاصیت errors را با یک شیء خالی ({}) مقدار دهی میکنیم و سپس در متد مدیریت ارسال فرم به سرور:
validate = () => { return { username: "Username is required." }; }; handleSubmit = e => { e.preventDefault(); const errors = this.validate(); console.log("Validation errors", errors); this.setState({ errors }); if (errors) { return; } // call the server console.log("Submitted!"); };
- اگر خطایی وجود داشت، به مرحلهی بعد که ارسال فرم به سمت سرور میباشد، نخواهیم رسید و کار را با یک return، خاتمه میدهیم.
- علت فراخوانی متد setState در اینجا، درخواست رندر مجدد فرم، با توجه به خطاهای اعتبارسنجی ممکنی است که به خاصیت errors، اضافه یا به روز رسانی کردهایم.
- نمونهای از خروجی متد validate را نیز در اینجا مشاهده میکنید که تشکیل شدهاست از یک شیء، که هر خاصیت آن، به نام یک فیلد موجود در فرم، اشاره میکند.
پیاده سازی یک اعتبارسنج ساده
در اینجا یک نمونه پیاده سازی ساده و ابتدایی منطق اعتبارسنجی فیلدهای فرم را ملاحظه میکنید:
validate = () => { const { account } = this.state; const errors = {}; if (account.username.trim() === "") { errors.username = "Username is required."; } if (account.password.trim() === "") { errors.password = "Password is required."; } return Object.keys(errors).length === 0 ? null : errors; };
- سپس یک شیء خالی error را تعریف کردهایم.
- در ادامه با توجه به اینکه مقادیر المانهای فرم در state وجود دارند، خالی بودن آنها را بررسی میکنیم. اگر خالی بودند، یک خاصیت جدید را با همان نام المان مورد بررسی، به شیء errors اضافه کرده و پیام خطایی را درج میکنیم.
- در نهایت این شیء errors و یا نال را (در صورت عدم وجود خطایی) بازگشت میدهیم.
برای آزمایش آن، پس از اجرای برنامه، یکبار بدون وارد کردن اطلاعاتی، بر روی دکمهی Login کلیک کنید؛ یکبار هم با وارد کردن اطلاعاتی در فیلدهای مختلف. در این بین کنسول توسعه دهندگان مرورگر را نیز جهت مشاهدهی شیءهای error لاگ شده، بررسی نمائید.
نمایش خطاهای اعتبارسنجی فیلدهای فرم
در قسمت قبل، کامپوننت جدید src\components\common\input.jsx را جهت کاهش کدهای تکراری تعاریف المانهای ورودی، ایجاد کردیم. در اینجا نیز میتوان کار نمایش خطاهای اعتبارسنجی را قرار داد:
import React from "react"; const Input = ({ name, label, value, error, onChange }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input value={value} onChange={onChange} id={name} name={name} type="text" className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
- سپس با توجه به نکتهی «رندر شرطی عناصر در کامپوننتهای React» در قسمت 5، اگر error مقداری داشته باشد و به true تفسیر شود، آنگاه به صورت خودکار، div ای که دارای کلاسهای بوت استرپی اخطار است به همراه متن خطا، رندر خواهد شد؛ در غیراینصورت هیچ div ای به صفحه اضافه نمیشود.
- اکنون متد رندر کامپوننت فرم لاگین را به صورت زیر تکمیل میکنیم:
render() { const { account, errors } = this.state; return ( <form onSubmit={this.handleSubmit}> <Input name="username" label="Username" value={account.username} onChange={this.handleChange} error={errors.username} /> <Input name="password" label="Password" value={account.password} onChange={this.handleChange} error={errors.password} /> <button className="btn btn-primary">Login</button> </form> ); }
تا اینجا اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، با کلیک بر روی دکمهی Login، خطاهای اعتبارسنجی به صورت زیر ظاهر میشوند:
در این حالت اگر هر دو فیلد را تکمیل کرده و بر روی دکمهی لاگین کلیک کنیم، به خطای زیر در کنسول توسعه دهندگان مرورگر میرسیم:
loginForm.jsx:55 Uncaught TypeError: Cannot read property 'username' of null at LoginForm.render (loginForm.jsx:55)
this.setState({ errors: errors || {} });
اعتبارسنجی فیلدهای یک فرم در حین ورود اطلاعات در آنها
تا اینجا نحوهی اعتبارسنجی فیلدهای ورودی را در حین submit بررسی کردیم. شبیه به همین روش را به حالت onChange و متد handleChange فرم لاگین که در قسمت قبل تکمیل کردیم نیز میتوان اعمال کرد:
handleChange = ({ currentTarget: input }) => { const errors = { ...this.state.errors }; //cloning an object const errorMessage = this.validateProperty(input); if (errorMessage) { errors[input.name] = errorMessage; } else { delete errors[input.name]; } const account = { ...this.state.account }; //cloning an object account[input.name] = input.value; this.setState({ account, errors }); };
- سپس اینبار فقط نیاز داریم اعتبار اطلاعات ورودی یک فیلد را بررسی کنیم و متد validate فعلی، فیلدهای کل فرم را با هم تعیین اعتبار میکند. به همین جهت متد جدید validateProperty را به صورت زیر تعریف میکنیم. اگر این متد خروجی داشت، خاصیت متناظر با آنرا در شیء errors به روز رسانی میکنیم؛ در غیراینصورت این خاصیت را از شیء errors حذف میکنیم تا پیام اشتباهی را نمایش ندهد. در نهایت توسط متد setState، مقدار خاصیت errors را با شیء errors جاری به روز رسانی میکنیم:
validateProperty = ({ name, value }) => { if (name === "username") { if (value.trim() === "") { return "Username is required."; } // ... } if (name === "password") { if (value.trim() === "") { return "Password is required."; } // ... } };
پس از ذخیره سازی این تغییرات، برای آزمایش آن، یکبار حرف a را بجای username وارد کنید و سپس آنرا حذف کنید. بلافاصله پیام خطای مرتبطی نمایش داده خواهد شد و اگر مجددا عبارتی را وارد کنیم، این پیام محو میشود که معادل قسمت delete در کدهای فوق است.
معرفی Joi
تا اینجا، هدف نمایش ساختار یک اعتبارسنج ساده بود. این روش مقیاس پذیر نیست و در ادامه آنرا با یک کتابخانهی اعتبارسنجی بسیار پیشرفته به نام Joi، جایگزین خواهیم کرد که نمونه مثالهای آنرا در اینجا میتوانید مشاهده کنید:
const Joi = require('@hapi/joi'); const schema = Joi.object({ username: Joi.string().alphanum().min(3).max(30).required(), password: Joi.string().pattern(/^[a-zA-Z0-9]{3,30}$/), email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } }) }); schema.validate({ username: 'abc', birth_year: 1994 });
برای نصب آن، پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install @hapi/joi --save > npm i --save-dev @types/hapi__joi
سپس به کامپوننت فرم لاگین مراجعه کرده و در ابتدای آن، Joi را import میکنیم:
import Joi from "@hapi/joi";
schema = { username: Joi.string() .required() .label("Username"), password: Joi.string() .required() .label("Password") };
سپس ابتدای متد validate قبلی را به صورت زیر بازنویسی میکنیم:
validate = () => { const { account } = this.state; const validationResult = Joi.object(this.schema).validate(account, { abortEarly: false }); console.log("validationResult", validationResult);
اکنون اگر برنامه را اجرا کرده و بدون ورود اطلاعاتی، بر روی دکمهی لاگین کلیک کنیم، خروجی زیر در کنسول توسعه دهندگان مرورگر ظاهر میشود:
همانطور که مشاهده میکنید، خروجی Joi، یک شیء است که اگر دارای خاصیت error بود، یعنی خطای اعتبارسنجی رخدادهاست. سپس باید خاصیت آرایهای details این شیء error را جهت یافتن خواص مشکل دار بررسی کرد. هر خاصیت در اینجا با path مشخص میشود. بنابراین قدم بعدی، تبدیل این ساختار، به ساختار شیء errors موجود در state کامپوننت جاری است تا مابقی برنامه بتواند بدون تغییری از آن استفاده کند.
نگاشت شیء دریافتی از Joi، به شیء errors موجود در state کامپوننت لاگین
خاصیت error شیء دریافتی از متد validate کتابخانهی Joi، تنها زمانی ظاهر میشود که خطایی وجود داشته باشد. همچنین خاصیت details آن نیز آرایهای از اشیاء با خواص message و path است. این path نیز یک آرایه است که اولین المان آن، نام خاصیت در حال بررسی است. اکنون میخواهیم این آرایه را تبدیل به یک شیء قابل درک برای برنامه کنیم:
validate = () => { const { account } = this.state; const validationResult = Joi.object(this.schema).validate(account, { abortEarly: false }); console.log("validationResult", validationResult); if (!validationResult.error) { return null; } const errors = {}; for (let item of validationResult.error.details) { errors[item.path[0]] = item.message; } return errors; };
اکنون اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، همانند قبل میتوان خطاهای اعتبارسنجی در حین submit را مشاهده کرد:
بازنویسی متد validateProperty توسط Joi
تا اینجا متد validate ساده و ابتدایی خود را با استفاده از امکانات کتابخانهی Joi، بازنویسی کردیم. اکنون نوبت بازنویسی متد اعتبارسنجی در حین تایپ اطلاعات است:
validateProperty = ({ name, value }) => { const userInputObject = { [name]: value }; const propertySchema = Joi.object({ [name]: this.schema[name] }); const { error } = propertySchema.validate(userInputObject, { abortEarly: true }); return error ? error.details[0].message : null; };
تا اینجا بجای this.state.account که به کل فرم اشاره میکند، شیء اختصاصیتر userInputObject را ایجاد کردهایم که معادل اطلاعات فیلد ورودی کاربر است. یک چنین نکتهای را در مورد schema نیز باید رعایت کرد. در اینجا نمیتوان اعتبارسنجی را با this.schema شروع کرد؛ چون این شیء نیز به اطلاعات کل فرم اشاره میکند و نه تک فیلدی که هم اکنون در حال کار با آن هستیم. بنابراین نیاز است یک Joi.object جدید را بر اساس name رسیده، از this.schema کلی، استخراج و تولید کرد؛ مانند propertySchema که مشاهده میکنید.
اکنون میتوان متد validate این شیء اسکیمای جدید را فراخوانی کرد و خاصیت error شیء حاصل از آنرا توسط Object Destructuring، استخراج نمود. در اینجا abortEarly به true تنظیم شدهاست (البته حالت پیشفرض آن است و نیازی به ذکر صریح آن نیست). علت اینجا است که نمایش خطاهای بیش از اندازه به کاربر، برای او گیج کننده خواهند بود؛ به همین جهت هربار، اولین خطای حاصل را به او نمایش میدهیم.
غیرفعالسازی دکمهی submit در صورت شکست اعتبارسنجیهای فرم
در ادامه میخواهیم دکمهی submit فرم لاگین، تا زمانیکه اعتبارسنجی آن با موفقیت به پایان برسد، غیرفعال باشد:
<button disabled={this.validate()} className="btn btn-primary"> Login </button>
فراخوانی setStateهای تعریف شدهی در متدهای رویدادگردان این فرم هستند که سبب رندر مجدد کامپوننت شده و در این بین در متد رندر، کار بررسی مجدد مقدار نهایی متد validate صورت میگیرد تا بر اساس آن، فعال یا غیرفعال بودن دکمهی Login، مشخص شود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-19.zip