اذهب إلى المحتوى

يوضح هذا المقال كيفية تعريف صفحات لإنشاء كائنات المؤلف والكتاب ونسخ الكتب للتعرّف أكثر على كيفية التعامل مع الاستمارات 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. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ القيمة بعد إدخالها وستُنقَل إلى صفحة تفاصيل المؤلف.

01 locallibary express author create empty

ملاحظة: إذا جرّبتَ تنسيقات إدخال مختلفة للتواريخ، فقد تجد أن التنسيق 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. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ الكتاب بعد إرسال كتاب صالح وستُنقَل إلى صفحة تفاصيل الكتاب:

02 locallibary express book create empty

استمارة إنشاء نسخة كتاب 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)‎. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ نسخة الكتاب بعد إرسال نسخة كتاب صالحة وستُنقَل إلى صفحة التفاصيل:

03 locallibary express bookinstance create empty

ترجمة -وبتصرُّف- للمقالات Create Author form و Create Book form و Create BookInstance form.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...