يوضح هذا المقال كيفية تعريف صفحات لإنشاء كائنات المؤلف والكتاب ونسخ الكتب للتعرّف أكثر على كيفية التعامل مع الاستمارات forms في إطار عمل Express.
استمارة إنشاء مؤلف
سنوضّح فيما يلي كيفية تعريف صفحة لإنشاء كائنات المؤلف Author
.
استيراد توابع التحقق من صحة البيانات Validation وتطهيرها Sanitization
يجب طلب الدوال التي نريدها لاستخدام express-validator كما هو الحال في استمارة نوع الكتاب. افتح الملف "/controllers/authorController.js"، وأضِف السطر التالي في بداية الملف قبل دوال الوِجهة Route:
const { body, validationResult } = require("express-validator");
متحكم وجهة Get
ابحث عن تابع المتحكم author_create_get()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية التي تؤدي إلى تقديم العرض author_form.pug وتمرير متغير العنوان title
:
// عرض استمارة إنشاء مؤلف في طلب GET exports.author_create_get = (req, res, next) => { res.render("author_form", { title: "Create Author" }); };
متحكم وجهة Post
ابحث عن تابع المتحكم author_create_post()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// معالجة إنشاء مؤلف Author في طلب POST exports.author_create_post = [ // التحقق من صحة الحقول وتطهيرها body("first_name") .trim() .isLength({ min: 1 }) .escape() .withMessage("First name must be specified.") .isAlphanumeric() .withMessage("First name has non-alphanumeric characters."), body("family_name") .trim() .isLength({ min: 1 }) .escape() .withMessage("Family name must be specified.") .isAlphanumeric() .withMessage("Family name has non-alphanumeric characters."), body("date_of_birth", "Invalid date of birth") .optional({ values: "falsy" }) .isISO8601() .toDate(), body("date_of_death", "Invalid date of death") .optional({ values: "falsy" }) .isISO8601() .toDate(), // طلب العملية بعد التحقق من صحة البيانات وتطهيرها asyncHandler(async (req, res, next) => { // استخراج أخطاء التحقق من صحة البيانات من الطلب const errors = validationResult(req); // إنشاء كائن مؤلف مع بيانات مُهرَّبة ومحذوف منها المسافات const author = new Author({ first_name: req.body.first_name, family_name: req.body.family_name, date_of_birth: req.body.date_of_birth, date_of_death: req.body.date_of_death, }); if (!errors.isEmpty()) { // توجد أخطاء، لذا اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة أو رسائل خطأ res.render("author_form", { title: "Create Author", author: author, errors: errors.array(), }); return; } else { // البيانات الواردة من الاستمارة صالحة // احفظ المؤلف await author.save(); // أعِد التوجيه إلى سجل مؤلف جديد res.redirect(author.url); } }), ];
تحذير: لا تتحقق من صحة الأسماء باستخدام isAlphanumeric()
كما فعلنا، نظرًا لوجود العديد من الأسماء التي تستخدم مجموعات محارف أخرى، ولكننا فعلنا ذلك لشرح كيفية استخدام أداة التحقق من صحة البيانات Validator، وكيفية ربطها بسلسلة تعاقبية مع أدوات التحقق الأخرى والإبلاغ عن الأخطاء.
تماثل بنية وسلوك هذه الشيفرة البرمجية تقريبًا شيفرة إنشاء كائن نوع الكتاب Genre
، إذ نتحقق أولًا من صحة البيانات ونطهرها، فإذا كانت البيانات غير صالحة سنعيد عرض الاستمارة مع البيانات التي أدخلها المستخدم في الأصل وقائمة برسائل الخطأ. وإذا كانت البيانات صالحة، سنحفظ سجل المؤلف الجديد ونعيد توجيه المستخدم إلى صفحة تفاصيل المؤلف.
لا نتحقق مما إذا كان كائن المؤلف Author
موجودًا مسبقًا قبل حفظه، وهذا يختلف عن معالج طلب Genre
من النوع Post، إذ يمكننا ذلك بالرغم من أنه يمكن أن يكون لدينا عدة مؤلفين بالاسم نفسه.
توضح شيفرة التحقق من صحة البيانات العديد من الميزات الجديدة التالية:
أولًا، يمكننا إنشاء سلسلة تعاقبية من أدوات التحقق باستخدام withMessage()
لتحديد رسالة الخطأ التي ستُعرَض في حالة فشل تابع التحقق السابق، مما يسهّل تقديم رسائل خطأ محددة دون الكثير من الشيفرة البرمجية المكررة.
[ // التحقق من صحة الحقول وتطهيرها body("first_name") .trim() .isLength({ min: 1 }) .escape() .withMessage("First name must be specified.") .isAlphanumeric() .withMessage("First name has non-alphanumeric characters."), // … ];
ثانيًا، يمكننا استخدام الدالة optional()
لإجراء عملية تحقق لاحقة فقط في حالة الإدخال في حقلٍ ما، مما يسمح بالتحقق من صحة الحقول الاختيارية، فمثلًا نتحقق فيما يلي من أن تاريخ الميلاد الاختياري هو تاريخ متوافق مع معيار ISO8601، إذ يعني تمرير كائن {values: "falsy"}
أننا سنقبل إما سلسلة نصية فارغة أو null
للقيمة الفارغة.
[ body("date_of_birth", "Invalid date of birth") .optional({ values: "falsy" }) .isISO8601() .toDate(), ];
ثالثًا، تُستلَم المعاملات من الطلب بوصفها سلاسلًا نصية، ويمكننا استخدام toDate()
أو toBoolean()
لتغيير هذه الأنواع إلى أنواع جافا سكريبت المناسبة كما هو موضح في نهاية سلسلة أدوات التحقق سابقًا.
العرض View
أنشئ العرض "/views/author_form.pug" وضع فيه النص التالي:
extends layout block content h1=title form(method='POST' action='') div.form-group label(for='first_name') First Name: input#first_name.form-control(type='text' placeholder='First name' name='first_name' required='true' value=(undefined===author ? '' : author.first_name) ) label(for='family_name') Family Name: input#family_name.form-control(type='text' placeholder='Family name' name='family_name' required='true' value=(undefined===author ? '' : author.family_name)) div.form-group label(for='date_of_birth') Date of birth: input#date_of_birth.form-control(type='date' name='date_of_birth' value=(undefined===author ? '' : author.date_of_birth) ) button.btn.btn-primary(type='submit') Submit if errors ul for error in errors li!= error.msg
تماثل بنية وسلوك هذا العرض ما هو موجود في قالب genre_form.pug، لذلك لن نشرحه مرةً أخرى.
ملاحظة: لا تدعم بعض المتصفحات حقل الإدخال type="date"
، لذلك لن تحصل على عنصر واجهة مستخدم منتقي التاريخ أو العنصر البديل الافتراضي dd/mm/yyyy
، ولكن ستحصل بدلًا من ذلك على حقل نص عادي فارغ. يتمثل أحد الحلول في إضافة السمة placeholder='dd/mm/yyyy'
صراحةً، بحيث تظل تحصل على معلومات حول تنسيق النص المطلوب في المتصفحات ذات القدرات الأقل.
التحدي: إضافة تاريخ الوفاة
يفتقد هذا القالب حقلًا لإدخال تاريخ الوفاة date_of_death
، لذا أنشئ هذا الحقل باتباع النمط نفسه لمجموعة استمارة تاريخ الميلاد.
كيف تبدو استمارة المؤلف؟
شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدّد رابط إنشاء مؤلف جديد Create new author. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ القيمة بعد إدخالها وستُنقَل إلى صفحة تفاصيل المؤلف.
ملاحظة: إذا جرّبتَ تنسيقات إدخال مختلفة للتواريخ، فقد تجد أن التنسيق yyyy-mm-dd
لا يتصرف بصورة صحيحة، لأن جافا سكريبت تتعامل مع سلاسل التاريخ بأنها تتضمن وقت 0 ساعة، ولكنها تتعامل أيضًا مع سلاسل التاريخ بهذا التنسيق (معيار ISO 8601) بوصفها تتضمن الوقت 0 ساعة بالتوقيت العالمي المنسَّق UTC بدلًا من التوقيت المحلي. إذا وقعت منطقتك الزمنية غرب توقيت UTC، فسيكون عرض التاريخ -لكونه محليًا- قبل يوم واحد من التاريخ الذي أدخلته، وهذه إحدى التعقيدات العديدة، مثل أسماء العائلات متعددة الكلمات والكتب متعددة المؤلفين التي لم نعالجها.
استمارة إنشاء كتاب
سنوضح كيفية تعريف صفحة أو استمارة لإنشاء كائنات الكتاب Book
، ويُعَد ذلك أكثر تعقيدًا قليلًا من صفحات المؤلف Author
أو نوع الكتاب Genre
المكافئة لها، لأننا نحتاج إلى الحصول على سجلات المؤلف Author
ونوع الكتاب Genre
المتوفرة وعرضها في استمارة الكتاب Book
.
استيراد توابع التحقق من صحة البيانات وتطهيرها
افتح الملف /controllers/bookController.js، وأضِف السطر التالي في بداية الملف قبل دوال الوِجهة Route:
const { body, validationResult } = require("express-validator");
متحكم وجهة Get
ابحث عن تابع المتحكم book_create_get()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// عرض استمارة إنشاء كتاب في طلب GET exports.book_create_get = asyncHandler(async (req, res, next) => { // الحصول على جميع المؤلفين وأنواع الكتب التي يمكننا استخدامها للإضافة إلى الكتاب const [allAuthors, allGenres] = await Promise.all([ Author.find().sort({ family_name: 1 }).exec(), Genre.find().sort({ name: 1 }).exec(), ]); res.render("book_form", { title: "Create Book", authors: allAuthors, genres: allGenres, }); });
نستخدم await
لانتظار نتيجة التابع Promise.all()
للحصول على جميع كائنات المؤلف Author
ونوع الكتاب Genre
على التوازي، وهو الأسلوب نفسه الذي استخدمناه عند عرض بيانات المكتبة، ثم تُمرَّر إلى العرض book_form.pug بوصفها متغيرات بالاسم authors
و genres
مع عنوان title
الصفحة.
متحكم وجهة Post
ابحث عن تابع المتحكم book_create_post()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// معالجة إنشاء كتاب في طلب POST exports.book_create_post = [ // تحويل نوع الكتاب إلى مصفوفة (req, res, next) => { if (!Array.isArray(req.body.genre)) { req.body.genre = typeof req.body.genre === "undefined" ? [] : [req.body.genre]; } next(); }, // التحقق من صحة الحقول وتطهيرها body("title", "Title must not be empty.") .trim() .isLength({ min: 1 }) .escape(), body("author", "Author must not be empty.") .trim() .isLength({ min: 1 }) .escape(), body("summary", "Summary must not be empty.") .trim() .isLength({ min: 1 }) .escape(), body("isbn", "ISBN must not be empty").trim().isLength({ min: 1 }).escape(), body("genre.*").escape(), // طلب العملية بعد التحقق من صحة البيانات وتطهيرها asyncHandler(async (req, res, next) => { // استخراج أخطاء التحقق من صحة البيانات من الطلب const errors = validationResult(req); // إنشاء كائن كتاب مع بيانات مُهرَّبة ومحذوف منها المسافات const book = new Book({ title: req.body.title, author: req.body.author, summary: req.body.summary, isbn: req.body.isbn, genre: req.body.genre, }); if (!errors.isEmpty()) { // توجد أخطاء، لذا اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة أو رسائل خطأ // احصل على جميع المؤلفين وأنواع الكتب للاستمارة const [allAuthors, allGenres] = await Promise.all([ Author.find().sort({ family_name: 1 }).exec(), Genre.find().sort({ name: 1 }).exec(), ]); // ميّز أنواع الكتب المختارة بوصفها محددة for (const genre of allGenres) { if (book.genre.includes(genre._id)) { genre.checked = "true"; } } res.render("book_form", { title: "Create Book", authors: allAuthors, genres: allGenres, book: book, errors: errors.array(), }); } else { // البيانات الواردة من الاستمارة صالحة، لذا احفظ الكتاب await book.save(); res.redirect(book.url); } }), ];
تماثل بنية وسلوك هذه الشيفرة البرمجية تقريبًا دوال وجهة Post لاستمارات النوع Genre
والكاتب Author
، إذ نتحقق أولًا من صحة البيانات ونطهّرها؛ فإذا كانت البيانات غير صالحة، فسنعيد عرض الاستمارة مع البيانات التي أدخلها المستخدم في الأصل وقائمة برسائل الخطأ؛ وإذا كانت البيانات صالحة، فسنحفظ سجل الكتاب Book
الجديد ونعيد توجيه المستخدم إلى صفحة تفاصيل الكتاب.
يتمثل الاختلاف الرئيسي فيما يتعلق بشيفرة معالجة الاستمارة الأخرى في كيفية تطهير معلومات نوع الكتاب، إذ تعيد الاستمارة مصفوفةً من عناصر Genre
، بينما تعيد سلسلة نصية بالنسبة للحقول الأخرى. نحوّل أولًا الطلب إلى مصفوفة (مطلوبة للخطوة التالية) للتحقق من صحة المعلومات.
[ // تحويل نوع الكتاب إلى مصفوفة (req, res, next) => { if (!Array.isArray(req.body.genre)) { req.body.genre = typeof req.body.genre === "undefined" ? [] : [req.body.genre]; } next(); }, // … ];
نستخدم بعد ذلك محرف بدل (*
) في أداة التطهير للتحقق من صحة كل إدخال من مصفوفة أنواع الكتاب بصورة فردية، إذ توضح الشيفرة التالية كيفية ترجمة ذلك إلى "تطهير كل عنصر له المفتاح genre
".
[ // … body("genre.*").escape(), // … ];
الاختلاف الأخير فيما يتعلق بشيفرة معالجة الاستمارة الأخرى هو أنه نحتاج إلى تمرير جميع أنواع الكتب والمؤلفين الموجودين إلى الاستمارة. نكرر عبر جميع أنواع الكتب ونضيف المعامل checked="true"
إلى تلك الأنواع التي كانت موجودة في بيانات Post (كما أعدنا الإنتاج في جزء الشيفرة التالي) لتمييز أنواع الكتب التي حدّدها المستخدم.
// تمييز أنواع الكتب المختارة بوصفها مُحدَّدة for (const genre of results.genres) { if (book.genre.includes(genre._id)) { // اختير نوع الكتاب الحالي، لذا اضبط الراية "checked" genre.checked = "true"; } }
العرض
أنشئ العرض "/views/book_form.pug" وضع فيه النص التالي:
extends layout block content h1= title form(method='POST' action='') div.form-group label(for='title') Title: input#title.form-control(type='text', placeholder='Name of book' name='title' required='true' value=(undefined===book ? '' : book.title) ) div.form-group label(for='author') Author: select#author.form-control(type='select', placeholder='Select author' name='author' required='true' ) for author in authors if book option(value=author._id selected=(author._id.toString()===book.author._id.toString() ? 'selected' : false) ) #{author.name} else option(value=author._id) #{author.name} div.form-group label(for='summary') Summary: textarea#summary.form-control(type='textarea', placeholder='Summary' name='summary' required='true') #{undefined===book ? '' : book.summary} div.form-group label(for='isbn') ISBN: input#isbn.form-control(type='text', placeholder='ISBN13' name='isbn' value=(undefined===book ? '' : book.isbn) required='true') div.form-group label Genre: div for genre in genres div(style='display: inline; padding-right:10px;') input.checkbox-input(type='checkbox', name='genre', id=genre._id, value=genre._id, checked=genre.checked ) label(for=genre._id) #{genre.name} button.btn.btn-primary(type='submit') Submit if errors ul for error in errors li!= error.msg
تماثل بنية وسلوك هذا العرض تقريبًا بنية وسلوك القالب genre_form.pug، ولكن تكمن الاختلافات الرئيسية في كيفية تقديم حقول نوع الاختيار: Author
و Genre
، إذ:
-
تُعرَض مجموعة أنواع الكتب بوصفها مربعات اختيار، وتستخدم قيمة
checked
التي ضبطناها في المتحكم لتحديد ما إذا كان يجب تحديد المربع أم لا. -
تُعرَض مجموعة المؤلفين بوصفها قائمة منسدلة مع اختيار فردي مرتبة أبجديًا، فإذا حدّد المستخدم مسبقًا مؤلف كتابٍ ما عند إصلاح قيم الحقول غير الصالحة بعد إرسال الاستمارة الأولية أو عند تحديث تفاصيل الكتاب مثلًا، فسيُعاد اختيار المؤلف عند عرض الاستمارة. نحدد في هذه الحالة المؤلف المراد اختياره من خلال موازنة معرّف خيار المؤلف الحالي مع القيمة التي أدخلها المستخدم مسبقًا والمُمرَّرة عبر المتغير
book
.
ملاحظة: إذا كان هناك خطأ في الاستمارة المُرسَلة، فسيكون معرّف مؤلف الكتاب الجديد ومعرّفات مؤلفي الكتب الموجودين مسبقًا من النوع Schema.Types.ObjectId
عند إعادة تصيير الاستمارة، لذا يجب تحويلها إلى سلاسل نصية أولًا للموازنة بينها.
كيف تبدو استمارة إنشاء كتاب؟
شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط إنشاء كتاب جديد Create new book. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ الكتاب بعد إرسال كتاب صالح وستُنقَل إلى صفحة تفاصيل الكتاب:
استمارة إنشاء نسخة كتاب BookInstance
سنوضح فيما يلي كيفية تعريف صفحة أو استمارة لإنشاء كائنات BookInstance
، وتشبه هذه الاستمارة إلى حد كبير الاستمارة التي استخدمناها لإنشاء كائنات الكتاب Book
.
استيراد توابع التحقق من صحة البيانات وتطهيرها
افتح الملف "/controllers/bookinstanceController.js"، وضِف السطر التالي في بداية الملف:
const { body, validationResult } = require("express-validator");
متحكم وجهة Get
اطلب الوحدة Book في الجزء العلوي من الملف، حيث تُعَد هذه الوحدة مطلوبة لأن كل نسخة كتاب BookInstance
مرتبطة بكتاب Book
معين.
const Book = require("../models/book");
ابحث عن تابع المتحكم bookinstance_create_get()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// عرض استمارة إنشاء نسخة كتاب BookInstance في طلب GET exports.bookinstance_create_get = asyncHandler(async (req, res, next) => { const allBooks = await Book.find({}, "title").sort({ title: 1 }).exec(); res.render("bookinstance_form", { title: "Create BookInstance", book_list: allBooks, }); });
يحصل المتحكم على قائمة بجميع الكتب (allBooks
) ويمرّرها عبر المتغير book_list
إلى العرض bookinstance_form.pug
مع المتغير title
. لاحظ عدم تحديد أي كتاب عندما أظهرنا الاستمارة لأول مرة، وبالتالي لم يُمرّر المتغير selected_book
إلى دالة التصيير ()render
، ونتيجةً لذلك سيكون للمتغير selected_book
قيمة undefined
في القالب.
متحكم وجهة Post
ابحث عن تابع المتحكم bookinstance_create_post()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// معالجة إنشاء نسخة كتاب في طلب POST exports.bookinstance_create_post = [ // التحقق من صحة الحقول وتطهيرها body("book", "Book must be specified").trim().isLength({ min: 1 }).escape(), body("imprint", "Imprint must be specified") .trim() .isLength({ min: 1 }) .escape(), body("status").escape(), body("due_back", "Invalid date") .optional({ values: "falsy" }) .isISO8601() .toDate(), // طلب العملية بعد التحقق من صحة البيانات وتطهيرها asyncHandler(async (req, res, next) => { // استخراج أخطاء التحقق من صحة البيانات من الطلب const errors = validationResult(req); // إنشاء كائن نسخة كتاب BookInstance مع بيانات مُهرَّبة ومحذوف منها المسافات const bookInstance = new BookInstance({ book: req.body.book, imprint: req.body.imprint, status: req.body.status, due_back: req.body.due_back, }); if (!errors.isEmpty()) { // توجد أخطاء // اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة ورسائل خطأ const allBooks = await Book.find({}, "title").sort({ title: 1 }).exec(); res.render("bookinstance_form", { title: "Create BookInstance", book_list: allBooks, selected_book: bookInstance.book._id, errors: errors.array(), bookinstance: bookInstance, }); return; } else { // البيانات الواردة من الاستمارة صالحة await bookInstance.save(); res.redirect(bookInstance.url); } }), ];
تماثل بنية وسلوك هذه الشيفرة البرمجية شيفرة إنشاء الكائنات الأخرى، إذ نتحقق أولًا من صحة البيانات ونطهرها؛ فإذا كانت البيانات غير صالحة، فسنعيد عرض الاستمارة مع البيانات التي أدخلها المستخدم في الأصل وقائمة برسائل الخطأ؛ وإذا كانت البيانات صالحة، فسنحفظ سجل نسخة الكتاب BookInstance
الجديد ونعيد توجيه المستخدم إلى صفحة التفاصيل.
العرض
أنشئ العرض "/views/bookinstance_form.pug" وضع فيه النص التالي:
extends layout block content h1=title form(method='POST' action='') div.form-group label(for='book') Book: select#book.form-control(type='select' placeholder='Select book' name='book' required='true') for book in book_list option(value=book._id, selected=(selected_book==book._id.toString() ? '' : false) ) #{book.title} div.form-group label(for='imprint') Imprint: input#imprint.form-control(type='text' placeholder='Publisher and date information' name='imprint' required='true' value=(undefined===bookinstance ? '' : bookinstance.imprint)) div.form-group label(for='due_back') Date when book available: input#due_back.form-control(type='date' name='due_back' value=(undefined===bookinstance ? '' : bookinstance.due_back_yyyy_mm_dd)) div.form-group label(for='status') Status: select#status.form-control(type='select' placeholder='Select status' name='status' required='true' ) option(value='Maintenance' selected=(undefined===bookinstance || bookinstance.status!='Maintenance' ? false:'selected')) Maintenance option(value='Available' selected=(undefined===bookinstance || bookinstance.status!='Available' ? false:'selected')) Available option(value='Loaned' selected=(undefined===bookinstance || bookinstance.status!='Loaned' ? false:'selected')) Loaned option(value='Reserved' selected=(undefined===bookinstance || bookinstance.status!='Reserved' ? false:'selected')) Reserved button.btn.btn-primary(type='submit') Submit if errors ul for error in errors li!= error.msg
تماثل بنية وسلوك هذا العرض تقريبًا بنية وسلوك القالب book_form.pug، لذا لن نشرحها مرةً أخرى.
ملاحظة: يجعل القالب السابق قيم الحالة Status ثابتة (في الصيانة Maintenance ومتوفر Available وغير ذلك)، ولا يتذكر القيم التي أدخلها المستخدم، فإذا رغبتَ في ذلك، ففكر في إعادة تقديم القائمة وتمرير بيانات الخيار من المتحكم وضبط القيمة المختارة عند إعادة عرض الاستمارة.
كيف تبدو استمارة إنشاء نسخة كتاب؟
شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط إنشاء نسخة كتاب جديدة Create new book instance (copy). إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ نسخة الكتاب بعد إرسال نسخة كتاب صالحة وستُنقَل إلى صفحة التفاصيل:
ترجمة -وبتصرُّف- للمقالات Create Author form و Create Book form و Create BookInstance form.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.