先決條件: | 完成所有以前的教學(xué)主題,包括 Express教學(xué)第5部分:顯示圖書館數(shù)據(jù) |
---|---|
目的: | 了解如何寫表單從用戶獲取數(shù)據(jù),以及使用此數(shù)據(jù)更新數(shù)據(jù)庫。 |
HTML表單是網(wǎng)絡(luò)上的一個或多個字段/小部件的組 頁面,可用于從用戶收集信息以提交到服務(wù)器。 表單是用于收集用戶輸入的靈活機(jī)制,因?yàn)榇嬖诳捎糜谳斎朐S多不同類型的數(shù)據(jù)(文本框,復(fù)選框,單選按鈕,日期選擇器等)的合適的表單輸入。表單也是與服務(wù)器共享數(shù)據(jù)的相對安全的方式 ,因?yàn)樗鼈冊试S我們在具有跨站點(diǎn)請求防止的 POST
請求中發(fā)送數(shù)據(jù)。
使用表單可能很復(fù)雜! 開發(fā)人員需要為表單編寫HTML,驗(yàn)證并正確清理服務(wù)器上(以及可能還在瀏覽器中)輸入的數(shù)據(jù),重新發(fā)布帶有錯誤消息的表單,以通知用戶任何無效字段,在成功提交數(shù)據(jù)時處理數(shù)據(jù) ,并最終以某種方式響應(yīng)用戶以指示成功。
在本教程中,我們將向您介紹如何在 Express 中執(zhí)行上述操作。 一路上,我們將擴(kuò)展 LocalLibrary 網(wǎng)站,允許用戶從庫中創(chuàng)建,編輯和刪除項目。
注意:我們沒有考慮如何將特定路由限制為經(jīng)過身份驗(yàn)證或授權(quán)的用戶,因此,此時任何用戶都可以對數(shù)據(jù)庫進(jìn)行更改。
首先簡要介紹 HTML表單。 考慮一個簡單的HTML表單,其中有一個文本字段用于輸入一些"團(tuán)隊"的名稱及其相關(guān)標(biāo)簽:
; width:399px;">
形式在HTML中定義為< form> ...< / form>
標(biāo)簽內(nèi)的元素集合,其包含至少一個輸入
type ="submit"。
<form action="/team_name_url/" method="post"> ? ? <label for="team_name">Enter name: </label> ? ? <input id="team_name" type="text" name="name_field" value="Default name for team."> ? ? <input type="submit" value="OK"> </form>
雖然這里我們只包括一個用于輸入團(tuán)隊名稱的文本字段,但是表單可以包含任何數(shù)量的其他輸入元素及其關(guān)聯(lián)的標(biāo)簽。 字段的 type
屬性定義將顯示哪種窗口小部件。 字段的 name
和 id
用于標(biāo)識JavaScript / CSS / HTML中的字段,而 value
定義字段的初始值 當(dāng)它第一次顯示。 匹配團(tuán)隊標(biāo)簽使用標(biāo)簽
標(biāo)簽(參見上面的"輸入名稱")指定,并使用 包含
字段的"-style:normal; font-weight:normal;"> code style ="font-style:normal; font-weight:normal;"> input 。 id
值的
submit
輸入將顯示為一個按鈕(默認(rèn)情況下) - 用戶可以按下它來將其他輸入元素包含的數(shù)據(jù)上傳到服務(wù)器(在這種情況下,只是 > team_name
)。 表單屬性定義用于在服務(wù)器上發(fā)送數(shù)據(jù)和數(shù)據(jù)目的地的HTTP 方法
( action
):
action
: The resource/URL where data is to be sent for processing when the form is submitted. If this is not set (or set to an empty string), then the form will be submitted back to the current page URL.method
: The HTTP method used to send the data: POST
or GET
. POST
method should always be used if the data is going to result in a change to the server's database, because this can be made more resistant to cross-site forgery request attacks.GET
method should only be used for forms that don't change user data (e.g. a search form). It is recommended for when you want to be able to bookmark or share the URL.表單處理使用了我們學(xué)習(xí)的用于顯示關(guān)于我們模型的信息的所有相同的技術(shù):路由將我們的請求發(fā)送到控制器功能,該功能執(zhí)行任何所需的數(shù)據(jù)庫動作,包括從模型讀取數(shù)據(jù),然后生成并返回HTML頁面。 使事情更復(fù)雜的是,服務(wù)器還需要能夠處理由用戶提供的數(shù)據(jù),并且如果有任何問題,重新顯示具有錯誤信息的表單。
處理表單請求的流程流程如下所示,從包含表單(如綠色所示)的頁面請求開始:20form%20handling.png"alt =""style ="height:649px; width:800px;">
如上圖所示,處理代碼需要做的主要事情是:
POST
request.通常使用用于初始顯示表單的 GET
路由和用于處理表單數(shù)據(jù)的驗(yàn)證和處理的相同路徑的 POST
路由來實(shí)現(xiàn)表單處理代碼。 這是本教程中將使用的方法!
Express本身不為表單處理操作提供任何特定的支持,但它可以使用中間件從表單處理 POST
和 GET
參數(shù),并驗(yàn)證/清除其值 。
在存儲表單的數(shù)據(jù)之前,必須對其進(jìn)行驗(yàn)證和清理:
在本教程中,我們將使用受歡迎的 express-validator 模塊來執(zhí)行驗(yàn)證和 我們的形式數(shù)據(jù)的衛(wèi)生。
通過在項目的根目錄中運(yùn)行以下命令來安裝模塊。
npm install express-validator --save
打開 ./ app.js ,然后在其他模塊之后導(dǎo)入 express-validator 模塊(靠近文件頂部,如圖所示)。
... var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var expressValidator = require('express-validator');
進(jìn)一步向下調(diào)用 app.use()
以將驗(yàn)證器添加到中間件堆棧。 這應(yīng)該在將 bodyParser
添加到中間件堆棧( express-validator 使用 body-parser 來訪問參數(shù))的代碼之后完成。
app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(expressValidator() ); // Add this after the bodyParser middlewares!
在本教程中,我們將主要使用以下API:
checkBody(parameter, message)
: Specifies a body (POST
) parameter to validate along with a message to be displayed if it fails the tests. The validation criteria are daisy chained to the checkBody()
method. For example, the first check below will test that the "name" parameter is alphanumeric and set an error message "Invalid name" if it is not. The second test checks that the age parameter has an integer value. req.checkBody('name', 'Invalid name').isAlpha(); req.checkBody('age', 'Invalid age').notEmpty().isInt();
sanitizeBody(parameter)
: Specifies a body parameter to sanitize. The sanitization operations are then daisy-chained to this method. For example, the escape()
sanitization operation below removes HTML characters?from the name variable that might be used in JavaScript cross-site scripting attacks. req.sanitizeBody('name').escape();
要運(yùn)行驗(yàn)證,請調(diào)用 req.validationErrors()。 這返回一個錯誤對象數(shù)組(如果沒有錯誤,則返回false),通常使用這樣的數(shù)組
var errors = req.validationErrors(); if (errors) { // Render the form using error information } else { // There are no errors so perform action with valid data (e.g. save record). }
數(shù)組中的每個錯誤對象都有參數(shù),消息和值的值。
{param: 'name', msg: 'Invalid name', value: '<received input>'}
注意: 該API還具有檢查和清理查詢和網(wǎng)址參數(shù)的方法(不僅僅是顯示的正文參數(shù))。 有關(guān)詳情,請參閱: express-validator (npm)。
我們將在下面實(shí)施 LocalLibrary 表單時介紹更多示例。
庫中的許多模型是相關(guān)的/依賴的 - 例如, Book
需要作者
和可 >也有一個或多個 Genres
。 這提出了如何處理用戶希望的情況的問題:
Genre
that is still being used by a Book
).對于這個項目,我們將簡化實(shí)現(xiàn),聲明一個表單只能:
Author
and Genre
instances before attempting to create any Book
objects).Book
until all associated BookInstance
objects have been deleted).注意:更強(qiáng)大的實(shí)施可能允許您在創(chuàng)建新對象時創(chuàng)建依賴對象,并隨時刪除任何對象(例如,刪除依賴對象,或刪除對 從數(shù)據(jù)庫刪除的對象)。
為了實(shí)現(xiàn)我們的表單處理代碼,我們將需要兩個具有相同URL模式的路由。 第一個( GET
)路由用于顯示用于創(chuàng)建對象的新空表單。 第二個路由( POST
)用于驗(yàn)證用戶輸入的數(shù)據(jù),然后保存信息并重定向到詳細(xì)信息頁面(如果數(shù)據(jù)有效)或重新顯示錯誤的表單(如果 數(shù)據(jù)無效)。
我們已在 /routes/catalog.js (在 學(xué)習(xí)/服務(wù)器端/ Express_Nodejs / routes">上一個教程)。 例如,類型路由如下所示:
/* GET request for creating a Genre. NOTE This must come before route that displays Genre (uses id) */ router.get('/genre/create', genre_controller.genre_create_get); /* POST request for creating Genre. */ router.post('/genre/create', genre_controller.genre_create_post);
這部分顯示我們?nèi)绾味x頁面來創(chuàng)建 Genre
對象(這是一個好的開始,因?yàn)?code> Genre 只有一個字段,其 >,沒有依賴)。 像任何其他頁面一樣,我們需要設(shè)置路由,控制器和視圖。
打開 /controllers/genreController.js 。 找到導(dǎo)出的 genre_create_get()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。 這只是呈現(xiàn) genre_form.pug 視圖,傳遞標(biāo)題變量。
// Display Genre create form on GET exports.genre_create_get = function(req, res, next) { ? ? ?? ? ? res.render('genre_form', { title: 'Create Genre'}); };
找到導(dǎo)出的 genre_create_post()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Handle Genre create on POST? exports.genre_create_post = function(req, res, next) { ? ?? ? ? //Check that the name field is not empty ? ? req.checkBody('name', 'Genre name required').notEmpty();? ? ?? ? ? //Trim and escape the name field.? ? ? req.sanitize('name').escape(); req.sanitize('name').trim(); ? ?? ? ? //Run the validators ? ? var errors = req.validationErrors(); ? ? //Create a genre object with escaped and trimmed data. ? ? var genre = new Genre( ? ? ? { name: req.body.name } ? ? ); ? ?? ? ? if (errors) { ? ? ? ? //If there are errors render the form again, passing the previously entered values and errors ? ? ? ? res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors}); ? ? return; ? ? }? ? ? else { ? ? ? ? // Data from form is valid. ? ? ? ? //Check if Genre with same name already exists ? ? ? ? Genre.findOne({ 'name': req.body.name }) ? ? ? ? ? ? .exec( function(err, found_genre) { ? ? ? ? ? ? ? ? ?console.log('found_genre: ' + found_genre); ? ? ? ? ? ? ? ? ?if (err) { return next(err); } ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?if (found_genre) {? ? ? ? ? ? ? ? ? ? ? ?//Genre exists, redirect to its detail page ? ? ? ? ? ? ? ? ? ? ?res.redirect(found_genre.url); ? ? ? ? ? ? ? ? ?} ? ? ? ? ? ? ? ? ?else { ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?genre.save(function (err) { ? ? ? ? ? ? ? ? ? ? ? ?if (err) { return next(err); } ? ? ? ? ? ? ? ? ? ? ? ?//Genre saved. Redirect to genre detail page ? ? ? ? ? ? ? ? ? ? ? ?res.redirect(genre.url); ? ? ? ? ? ? ? ? ? ? ?}); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?} ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?}); ? ? } };
該代碼的第一部分定義了一個驗(yàn)證器來檢查名稱字段不為空(如果為空,則顯示錯誤消息),清除和修剪(刪除字符串兩端的空格)值,然后運(yùn)行驗(yàn)證器 。
//Check that the name field is not empty req.checkBody('name', 'Genre name required').notEmpty();? ? ?? //Trim and escape the name field.? req.sanitize('name').escape(); req.sanitize('name').trim(); ? ?? //Run the validators var errors = req.validationErrors();
如果存在錯誤,我們再次使用表單呈現(xiàn)模板,此時還傳遞一個帶有用戶傳遞的任何(已清除)值的變量以及包含錯誤信息的對象。
//Create a genre object with escaped and trimmed data. var genre = new Genre({ name: req.body.name }); if (errors) { res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors}); ? ? return; ? ? }?
如果數(shù)據(jù)有效,那么我們檢查是否存在具有相同名稱的 Genre
(我們不想創(chuàng)建重復(fù)的)。 如果它,我們重定向到現(xiàn)有的流派的詳細(xì)信息頁面。 如果沒有,我們保存新的流派,并重定向到其詳細(xì)頁面。
//Check if Genre with same name already exists Genre.findOne({ 'name': req.body.name }) .exec( function(err, found_genre) { console.log('found_genre: '+found_genre) if (err) { return next(err); } ? ? ? ? ? ? ? ? ? if (found_genre) {? ? ? ? ? ? ? //Genre exists, redirect to its detail page ? ? ? ? ? ? res.redirect(found_genre.url); ? ? ? ? ? ? } ? ? ? ? else { ? ? ? ? genre.save(function (err) { ? ? ? ? ? ? if (err) { return next(err); } ? ? ? ? ? ? ? ? //Genre saved. Redirect to genre detail page ? ? ? ? ? ? ? ? ? ?res.redirect(genre.url); ? ? ? ? ? ? ? ? }); ? ? ? ? } ? ? ? ? ? ? ? ? ? });
在 GET
和 POST
控制器(路由)中都呈現(xiàn)相同的視圖。 在 GET
的情況下,表單是空的,我們只是傳遞一個標(biāo)題變量。 在 POST
情況下,用戶以前輸入了無效數(shù)據(jù) - 在 genre
變量中,我們傳回輸入的數(shù)據(jù)(已清理)和 errors
變量傳回錯誤消息。
res.render('genre_form', { title: 'Create Genre'}); res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors});
創(chuàng)建 /views/genre_form.pug 并在下面的文本中復(fù)制。
extends layout block content ? h1 #{title} ? form(method='POST' action='') ? ? div.form-group ? ? ? label(for='name') Genre: ? ? ? input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name) ) ? ? button.btn.btn-primary(type='submit') Submit ? if errors? ? ? ul ? ? ? for error in errors ? ? ? ? li!= error.msg
這個模板的大部分將從我們以前的教程中熟悉。 首先,我們擴(kuò)展 layout.pug 基本模板,并覆蓋名為"內(nèi)容"的塊
。 然后我們有一個標(biāo)題與從控制器(通過 render()
方法)傳遞的 title
。
接下來,我們使用HTML表單的pug代碼,使用 POST
方法
將數(shù)據(jù)發(fā)送到服務(wù)器,因?yàn)?code> action 空字符串,將數(shù)據(jù)發(fā)送到與頁面相同的URL。
表單定義了一個名為"name"的類型為"text"的單個必填字段。 字段的默認(rèn)值取決于是否定義了 genre
變量(參見上面粗體突出顯示的文本)。 如果從 GET
路由調(diào)用它將是空的,因?yàn)檫@是一個新的形式。 如果從 POST
路由調(diào)用,它將包含用戶最初輸入的(無效)值。
頁面的最后一部分是錯誤代碼。 如果已經(jīng)定義了錯誤變量(換句話說,當(dāng)模板在 GET
路由上呈現(xiàn)時,此部分不會出現(xiàn)),這只是打印錯誤列表。
注意:這只是一種呈現(xiàn)錯誤的方法。 您還可以從錯誤變量中獲取受影響字段的名稱,并使用這些字段來控制錯誤消息的呈現(xiàn)位置,是否應(yīng)用自定義CSS等。
運(yùn)行應(yīng)用程序,打開瀏覽器 http:// localhost:3000 / ,然后選擇創(chuàng)建新流派 鏈接。 如果一切設(shè)置正確,您的網(wǎng)站應(yīng)該看起來像下面的屏幕截圖。 輸入值后,應(yīng)保存該值,您將進(jìn)入類型詳細(xì)信息頁面。
; width:800px;">
我們對服務(wù)器端驗(yàn)證的唯一錯誤是類型字段不能為空。 下面的屏幕截圖顯示了如果您沒有提供類型(以紅色突出顯示),錯誤列表會是什么樣子。
margin:0px auto; width:400px;">
請注意:更好的實(shí)施將驗(yàn)證該字段在客戶端不為空。 將值 required =\'true\'
添加到字段定義中:
input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name), required='true' )
本節(jié)說明如何定義一個用于創(chuàng)建 Author
對象的頁面。
打開 /controllers/authorController.js 。 找到導(dǎo)出的 author_create_get()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。 這只是呈現(xiàn) author_form.pug 視圖,傳遞 title
變量。
// Display Author create form on GET exports.author_create_get = function(req, res, next) { ? ? ?? ? ? res.render('author_form', { title: 'Create Author'}); };
找到導(dǎo)出的 author_create_post()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Handle Author create on POST? exports.author_create_post = function(req, res, next) { ? ? ? ? req.checkBody('first_name', 'First name must be specified.').notEmpty(); //We won't force Alphanumeric, because people might have spaces. ? ? req.checkBody('family_name', 'Family name must be specified.').notEmpty(); ? ? req.checkBody('family_name', 'Family name must be alphanumeric text.').isAlpha(); ? ? req.checkBody('date_of_birth', 'Invalid date').optional({ checkFalsy: true }).isDate(); ? ? req.checkBody('date_of_death', 'Invalid date').optional({ checkFalsy: true }).isDate(); ? ?? ? ? req.sanitize('first_name').escape(); ? ? req.sanitize('family_name').escape(); req.sanitize('first_name').trim(); ? ? req.sanitize('family_name').trim(); ? ? req.sanitize('date_of_birth').toDate(); ? ? req.sanitize('date_of_death').toDate(); ? ? var errors = req.validationErrors(); ? ?? ? ? var 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) { ? ? ? ? res.render('author_form', { title: 'Create Author', author: author, errors: errors}); ? ? return; ? ? }? ? ? else { ? ? // Data from form is valid ? ?? ? ? ? ? author.save(function (err) { ? ? ? ? ? ? if (err) { return next(err); } ? ? ? ? ? ? ? ?//successful - redirect to new author record. ? ? ? ? ? ? ? ?res.redirect(author.url); ? ? ? ? ? ? }); ? ? } };
找到導(dǎo)出的 author_create_post()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。...
注意:與Genre帖子處理程序不同,我們不會在保存之前檢查 Author
對象是否已存在。 可以說,我們應(yīng)該,雖然現(xiàn)在我們可以有多個作者有相同的名字。
驗(yàn)證代碼演示了兩個新功能:
optional()
function to run a subsequent validation only if a field has been entered (this allows us to validate optional fields). For example, below we check that the optional date of birth is a date (the checkFalsy
flag means that we'll accept either an empty string or null
as an empty value). req.checkBody('date_of_birth', 'Invalid date').optional({ checkFalsy: true }).isDate();
toDate()
(or toBoolean()
, etc.) to cast these to the proper JavaScript types. req.sanitize('date_of_birth').toDate()
創(chuàng)建 /views/author_form.pug 并在下面的文字中復(fù)制。
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 (Christian) last' 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 (surname)' 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
此視圖的結(jié)構(gòu)和行為與 genre_form.pug 模板完全相同,因此我們不再對其進(jìn)行說明。
注意:某些瀏覽器不支持輸入 type ="date"
,因此您將無法使用datepicker小部件或默認(rèn)的 mm / yyyy 占位符,而是得到一個空的純文本字段。一個解決方法是明確添加屬性 placeholder =\'dd / mm / yyyy\'
不夠功能的瀏覽器,你仍然會得到所需的文本格式的信息。
挑戰(zhàn):上述范本缺少輸入 date_of_death
的欄位。 創(chuàng)建字段遵循與出生表格組的日期相同的模式!
運(yùn)行應(yīng)用程序,打開瀏覽器 http:// localhost:3000 / ,然后選擇創(chuàng)建新作者 鏈接。 如果一切設(shè)置正確,您的網(wǎng)站應(yīng)該看起來像下面的屏幕截圖。 輸入值后,應(yīng)保存該值,您將轉(zhuǎn)到作者詳細(xì)信息頁面。
本節(jié)說明如何定義一個頁面/表單來創(chuàng)建 Book
對象。 這比等效的 Author
或 Genre
頁面復(fù)雜一些,因?yàn)槲覀冃枰@取并顯示可用的 Author
和 代碼>記錄存儲在我們的
Book
表單中。
打開 /controllers/bookController.js 。 找到導(dǎo)出的 book_create_get()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Display book create form on GET exports.book_create_get = function(req, res, next) {? ? ? ?? ? ? //Get all authors and genres, which we can use for adding to our book. ? ? async.parallel({ ? ? ? ? authors: function(callback) { ? ? ? ? ? ? Author.find(callback); ? ? ? ? }, ? ? ? ? genres: function(callback) { ? ? ? ? ? ? Genre.find(callback); ? ? ? ? }, ? ? }, function(err, results) { ? ? ? ? if (err) { return next(err); } ? ? ? ? res.render('book_form', { title: 'Create Book',authors:results.authors, genres:results.genres }); ? ? }); ? ?? };
這使用異步模塊(如 Express教程第5部分:顯示庫數(shù)據(jù)中所述)以獲取所有作者
和類型
對象。 然后將它們作為名為 authors
和 genres
的變量傳遞到視圖 book_form.pug
code> title )。
找到導(dǎo)出的 book_create_post()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Handle book create on POST? exports.book_create_post = function(req, res, next) { ? ? req.checkBody('title', 'Title must not be empty.').notEmpty(); ? ? req.checkBody('author', 'Author must not be empty').notEmpty(); ? ? req.checkBody('summary', 'Summary must not be empty').notEmpty(); ? ? req.checkBody('isbn', 'ISBN must not be empty').notEmpty(); ? ?? ? ? req.sanitize('title').escape(); ? ? req.sanitize('author').escape(); ? ? req.sanitize('summary').escape(); ? ? req.sanitize('isbn').escape(); req.sanitize('title').trim(); ? ? req.sanitize('author').trim(); ? ? req.sanitize('summary').trim(); ? ?req.sanitize('isbn').trim(); ? ? req.sanitize('genre').escape(); ? ?? ? ? var book = new Book( ? ? ? { title: req.body.title,? ? ? ? ? author: req.body.author,? ? ? ? ? summary: req.body.summary, ? ? ? ? isbn: req.body.isbn, ? ? ? ? genre: (typeof req.body.genre==='undefined') ? [] : req.body.genre.split(",") ? ? ? ?}); ? ? ? ? ? ? console.log('BOOK: '+book); ? ?? ? ? var errors = req.validationErrors(); ? ? if (errors) { ? ? ? ? // Some problems so we need to re-render our book ? ? ? ? //Get all authors and genres for form ? ? ? ? async.parallel({ ? ? ? ? ? ? authors: function(callback) { ? ? ? ? ? ? ? ? Author.find(callback); ? ? ? ? ? ? }, ? ? ? ? ? ? genres: function(callback) { ? ? ? ? ? ? ? ? Genre.find(callback); ? ? ? ? ? ? }, ? ? ? ? }, function(err, results) { ? ? ? ? ? ? if (err) { return next(err); } ? ? ? ? ? ?? ? ? ? ? ? ? // Mark our selected genres as checked ? ? ? ? ? ? for (i = 0; i < results.genres.length; i++) { ? ? ? ? ? ? ? ? if (book.genre.indexOf(results.genres[i]._id) > -1) { ? ? ? ? ? ? ? ? ? ? //Current genre is selected. Set "checked" flag. ? ? ? ? ? ? ? ? ? ? results.genres[i].checked='true'; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? ? ? res.render('book_form', { title: 'Create Book',authors:results.authors, genres:results.genres, book: book, errors: errors }); ? ? ? ? }); ? ? }? ? ? else { ? ? // Data from form is valid. ? ? // We could check if book exists already, but lets just save. ? ?? ? ? ? ? book.save(function (err) { ? ? ? ? ? ? if (err) { return next(err); } ? ? ? ? ? ? ? ?//successful - redirect to new book record. ? ? ? ? ? ? ? ?res.redirect(book.url); ? ? ? ? ? ? }); ? ? } };
這段代碼的結(jié)構(gòu)和行為幾乎完全一樣創(chuàng)建的類型/ code>對象。 首先,我們驗(yàn)證和清理數(shù)據(jù)。如果數(shù)據(jù)無效,那么我們與最初由用戶和錯誤消息的列表中輸入的數(shù)據(jù)一起重新顯示的形式。 如果數(shù)據(jù)有效,我們保存新的
Book
記錄,并將用戶重定向到圖書詳細(xì)信息頁面。
同樣,與其他表單處理代碼的主要區(qū)別是,我們需要將所有現(xiàn)有的流派和作者傳遞給表單。 為了標(biāo)記用戶檢查的類型,我們遍歷所有類型,并將 checked =\'true\'
參數(shù)添加到我們的帖子數(shù)據(jù)中(如下面的代碼片段 )。
// Mark our selected genres as checked for (i = 0; i < results.genres.length; i++) { if (book.genre.indexOf(results.genres[i]._id) > -1) { //Current genre is selected. Set "checked" flag. results.genres[i].checked='true'; ? ? } }
創(chuàng)建 /views/book_form.pug 并在下面的文字中復(fù)制。
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: ? ? ? input#summary.form-control(type='textarea', placeholder='Summary' name='summary' value=(undefined===book ? '' : book.summary) required='true') ? ? 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
視圖結(jié)構(gòu)和行為與 genre_form.pug 模板幾乎相同。
主要區(qū)別在于我們?nèi)绾螌?shí)現(xiàn)選擇類型字段:作者和類型。
checked
value we set in the controller to determine whether or not the box should be selected.book
variable). This is highlighted above! 請注意:必須使用字符串值進(jìn)行比較,如上所示。 直接比較id值不起作用!
運(yùn)行應(yīng)用程序,打開瀏覽器 http:// localhost:3000 / ,然后選擇創(chuàng)建新書 鏈接。 如果一切設(shè)置正確,您的網(wǎng)站應(yīng)該看起來像下面的屏幕截圖。 提交有效的書籍后,系統(tǒng)會儲存書籍,系統(tǒng)會將您導(dǎo)向書籍詳細(xì)資料頁面。
本節(jié)說明如何定義一個頁面/表單來創(chuàng)建 BookInstance
對象。 這非常像我們用來創(chuàng)建 Book
對象的形式。
打開 /controllers/bookinstanceController.js 。
在文件頂部,需要 Book 模塊(因?yàn)槊總€ BookInstance
與特定的 Book
相關(guān)聯(lián))。
var Book = require('../models/book');
找到導(dǎo)出的 bookinstance_create_get()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Display BookInstance create form on GET exports.bookinstance_create_get = function(req, res, next) { ? ? ?? ? ? Book.find({},'title') ? ? .exec(function (err, books) { ? ? ? if (err) { return next(err); } ? ? ? //Successful, so render ? ? ? res.render('bookinstance_form', {title: 'Create BookInstance', book_list:books } ); ? ? }); ? ?? };
控制器會獲取所有圖書的列表( book_list
),并將其傳遞到視圖 bookinstance_form.pug
/ code>)
找到導(dǎo)出的 bookinstance_create_post()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Handle BookInstance create on POST? exports.bookinstance_create_post = function(req, res, next) { ? ? req.checkBody('book', 'Book must be specified').notEmpty(); //We won't force Alphanumeric, because people might have spaces. ? ? req.checkBody('imprint', 'Imprint must be specified').notEmpty(); ? ? req.checkBody('due_back', 'Invalid date').optional({ checkFalsy: true }).isDate(); ? ?? ? ? req.sanitize('book').escape(); ? ? req.sanitize('imprint').escape(); ? ? req.sanitize('status').escape(); req.sanitize('book').trim(); ? ? req.sanitize('imprint').trim(); ? ? req.sanitize('status').trim(); ? ? req.sanitize('due_back').toDate(); ? ?? ? ? var bookinstance = new BookInstance( ? ? ? { book: req.body.book, ? ? ? ? imprint: req.body.imprint,? ? ? ? ? status: req.body.status, ? ? ? ? due_back: req.body.due_back ? ? ? ?}); ? ? var errors = req.validationErrors(); ? ? if (errors) { ? ? ? ?? ? ? ? ? Book.find({},'title') ? ? ? ? .exec(function (err, books) { ? ? ? ? ? if (err) { return next(err); } ? ? ? ? ? //Successful, so render ? ? ? ? ? res.render('bookinstance_form', { title: 'Create BookInstance', book_list : books, selected_book : bookinstance.book._id , errors: errors, bookinstance:bookinstance }); ? ? ? ? }); ? ? ? ? return; ? ? }? ? ? else { ? ? // Data from form is valid ? ?? ? ? ? ? bookinstance.save(function (err) { ? ? ? ? ? ? if (err) { return next(err); } ? ? ? ? ? ? ? ?//successful - redirect to new author record. ? ? ? ? ? ? ? ?res.redirect(bookinstance.url); ? ? ? ? ? ? });? ? ? } };
此代碼的結(jié)構(gòu)和行為與創(chuàng)建其他對象的結(jié)構(gòu)和行為相同。 首先,我們驗(yàn)證和清理數(shù)據(jù)。 如果數(shù)據(jù)無效,我們將重新顯示表單以及用戶最初輸入的數(shù)據(jù)和錯誤消息列表。 如果數(shù)據(jù)有效,我們保存新的 BookInstance
記錄,并將用戶重定向到詳細(xì)信息頁面。
創(chuàng)建 /views/bookinstance_form.pug 并在下面的文字中復(fù)制。
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 ? 'selected' : 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)) ? ? ? ? ? ?? ? ? div.form-group ? ? ? label(for='status') Status: ? ? ? select#status.form-control(type='select', placeholder='Select status' name='status' required='true' ) ? ? ? ? option(value='Maintenance') Maintenance ? ? ? ? option(value='Available') Available ? ? ? ? option(value='Loaned') Loaned ? ? ? ? option(value='Reserved') Reserved ? ? button.btn.btn-primary(type='submit') Submit ? if errors? ? ? ul ? ? ? for error in errors ? ? ? ? li!= error.msg
視圖結(jié)構(gòu)和行為與 book_form.pug 模板幾乎相同,因此我們不再重復(fù)。
注意:上述模板會對狀態(tài)值(維護(hù),可用等)進(jìn)行硬編碼,但不會"記住"用戶輸入的值。 如果您愿意,請考慮重新實(shí)現(xiàn)列表,從控制器傳遞選項數(shù)據(jù),并在重新顯示表單時設(shè)置所選值。
運(yùn)行應(yīng)用程序并打開瀏覽器以 http:// localhost:3000 / 。 然后選擇創(chuàng)建新圖書實(shí)例(復(fù)制)鏈接。 如果一切設(shè)置正確,您的網(wǎng)站應(yīng)該看起來像下面的屏幕截圖。 提交有效的 BookInstance
后,應(yīng)保存,您將進(jìn)入詳細(xì)信息頁面。
此部分顯示如何定義要刪除作者
對象的頁面。
如表單設(shè)計部分中所述,我們的策略將是只允許刪除未被其他對象引用的對象(在這種情況下,我們不允許 作者
如果被 Book
引用就被刪除)。 在實(shí)現(xiàn)方面,這意味著表單需要確認(rèn)在刪除作者之前沒有關(guān)聯(lián)的圖書。 如果有相關(guān)聯(lián)的圖書,它應(yīng)該顯示它們,并聲明它們必須在刪除 Author
對象之前刪除。
打開 /controllers/authorController.js 。 找到導(dǎo)出的 author_delete_get()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Display Author delete form on GET exports.author_delete_get = function(req, res, next) { ? ? ?? ? ? async.parallel({ ? ? ? ? author: function(callback) { ? ?? ? ? ? ? ? ? Author.findById(req.params.id).exec(callback); ? ? ? ? }, ? ? ? ? authors_books: function(callback) { ? ? ? ? ? Book.find({ 'author': req.params.id }).exec(callback); ? ? ? ? }, ? ? }, function(err, results) { ? ? ? ? if (err) { return next(err); } ? ? ? ? //Successful, so render ? ? ? ? res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.authors_books } ); ? ? }); ? ?? };
控制器獲取要從URL參數(shù)( req.params.id
)中刪除的 Author
實(shí)例的ID。 它使用 async.parallel()
方法來并行獲取作者記錄和所有相關(guān)聯(lián)的圖書。 當(dāng)兩個操作完成后,它會呈現(xiàn) author_delete
.pug 視圖,傳遞 title
作者和 author_books
。
找到導(dǎo)出的 author_delete_post()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Handle Author delete on POST? exports.author_delete_post = function(req, res, next) { ? ? req.checkBody('authorid', 'Author id must exist').notEmpty(); ? ? ?? ? ? async.parallel({ ? ? ? ? author: function(callback) { ? ?? ? ? ? ? ? ? Author.findById(req.body.authorid).exec(callback); ? ? ? ? }, ? ? ? ? authors_books: function(callback) { ? ? ? ? ? Book.find({ 'author': req.body.authorid },'title summary').exec(callback); ? ? ? ? }, ? ? }, function(err, results) { ? ? ? ? if (err) { return next(err); } ? ? ? ? //Success ? ? ? ? if (results.authors_books>0) { ? ? ? ? ? ? //Author has books. Render in same way as for GET route. ? ? ? ? ? ? res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.authors_books } ); ? ? ? ? ? ? return; ? ? ? ? } ? ? ? ? else { ? ? ? ? ? ? //Author has no books. Delete object and redirect to the list of authors. ? ? ? ? ? ? Author.findByIdAndRemove(req.body.authorid, function deleteAuthor(err) { ? ? ? ? ? ? ? ? if (err) { return next(err); } ? ? ? ? ? ? ? ? //Success - got to author list ? ? ? ? ? ? ? ? res.redirect('/catalog/authors'); ? ? ? ? ? ? }); ? ? ? ? } ? ? }); };
首先,我們驗(yàn)證是否已經(jīng)提供了一個ID(通過表單主體參數(shù)發(fā)送,而不是使用URL中的版本)。 然后,我們以與 GET
路由相同的方式獲取作者及其相關(guān)聯(lián)的書籍。 如果沒有書,那么我們刪除作者對象并重定向到所有作者的列表。 如果還有書,我們只需重新呈現(xiàn)表單,傳入作者和要刪除的書籍列表。
創(chuàng)建 /views/author_delete.pug 并在下面的文字中復(fù)制。
extends layout block content ? h1 #{title}: #{author.name} ? p= author.lifespan ?? ? if author_books.length ?? ? ? p #[strong Delete the following books before attempting to delete this author.] ?? ? ? div(style='margin-left:20px;margin-top:20px') ? ? ? h4 Books ? ?? ? ? ? dl ? ? ? each book in author_books ? ? ? ? dt? ? ? ? ? ? a(href=book.url) #{book.title} ? ? ? ? dd #{book.summary} ? else ? ? p Do you really want to delete this Author? ? ?? ? ? form(method='POST' action='') ? ? ? div.form-group ? ? ? ? input#authorid.form-control(type='hidden',name='authorid', required='true', value=author._id ) ? ? ? button.btn.btn-primary(type='submit') Delete
視圖擴(kuò)展了布局模板,覆蓋了名為 content
的塊。 在頂部顯示作者詳細(xì)信息。 然后它包括基于 author_books
( if
和 else
)。
POST
request and it will be deleted.接下來,我們將刪除按鈕添加到作者詳細(xì)信息視圖中(詳細(xì)信息頁是刪除記錄的好地方)。
注意:在完整實(shí)施中,只有授權(quán)用戶才能看到此按鈕。 但是在這一點(diǎn)上,我們沒有一個授權(quán)系統(tǒng)到位!
打開 author_detail.pug 視圖,然后在底部添加以下幾行。
hr ? p a(href=author.url+'/delete') Delete author
此時,該按鈕應(yīng)如下所示出現(xiàn)在作者詳細(xì)信息頁面上。
margin:0px auto; width:500px;">
運(yùn)行應(yīng)用程序并打開瀏覽器以 http:// localhost:3000 / 。 然后選擇所有作者鏈接,然后選擇特定作者。 最后,選擇刪除作者鏈接。
如果作者沒有書,你會看到一個這樣的頁面。 按下刪除后,服務(wù)器將刪除作者并重定向到作者列表。
margin:0px auto; width:600px;">
如果作者有書,那么你將看到如下的視圖。 然后,您可以從其詳細(xì)信息頁面中刪除圖書(一旦代碼實(shí)施!)
margin:0px auto; width:500px;">
注意:刪除對象的其他頁面可以以大致相同的方式實(shí)施。 我們已經(jīng)離開了這是一個挑戰(zhàn)。
本節(jié)說明如何定義一個頁面來更新 Book
對象。 更新圖書時的表單處理非常類似于創(chuàng)建圖書,只不過您必須使用數(shù)據(jù)庫中的值填充 GET
路由中的表單。
打開 /controllers/bookController.js 。 找到導(dǎo)出的 book_update_get()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Display book update form on GET exports.book_update_get = function(req, res, next) { ? ? req.sanitize('id').escape(); req.sanitize('id').trim(); ? ? //Get book, authors and genres for form ? ? async.parallel({ ? ? ? ? book: function(callback) { ? ? ? ? ? ? Book.findById(req.params.id).populate('author').populate('genre').exec(callback); ? ? ? ? }, ? ? ? ? authors: function(callback) { ? ? ? ? ? ? Author.find(callback); ? ? ? ? }, ? ? ? ? genres: function(callback) { ? ? ? ? ? ? Genre.find(callback); ? ? ? ? }, ? ? }, function(err, results) { ? ? ? ? if (err) { return next(err); } ? ? ? ? ? ?? ? ? ? ? // Mark our selected genres as checked ? ? ? ? for (var all_g_iter = 0; all_g_iter < results.genres.length; all_g_iter++) { ? ? ? ? ? ? for (var book_g_iter = 0; book_g_iter < results.book.genre.length; book_g_iter++) { ? ? ? ? ? ? ? ? if (results.genres[all_g_iter]._id.toString()==results.book.genre[book_g_iter]._id.toString()) { ? ? ? ? ? ? ? ? ? ? results.genres[all_g_iter].checked='true'; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? res.render('book_form', { title: 'Update Book', authors:results.authors, genres:results.genres, book: results.book }); ? ? }); ? ?? };
控制器從URL參數(shù)( req.params.id
)獲取要更新的 Book
的id。 它使用 async.parallel()
方法來獲取指定的 Book
記錄(填充其流派和作者字段)以及所有Author和Genre對象的列表。 當(dāng)所有操作完成后,將當(dāng)前選定的類型標(biāo)記為選中,然后呈現(xiàn) author_form.pug 視圖,傳遞 title
代碼>和所有 genres
。
找到導(dǎo)出的 book_update_post()
控制器方法,并將其替換為以下代碼(更改的代碼以粗體顯示)。
// Handle book update on POST? exports.book_update_post = function(req, res, next) { ? ?? ? ? //Sanitize id passed in.? ? ? req.sanitize('id').escape(); req.sanitize('id').trim(); ? ?? ? ? //Check other data ? ? req.checkBody('title', 'Title must not be empty.').notEmpty(); ? ? req.checkBody('author', 'Author must not be empty').notEmpty(); ? ? req.checkBody('summary', 'Summary must not be empty').notEmpty(); ? ? req.checkBody('isbn', 'ISBN must not be empty').notEmpty(); ? ?? ? ? req.sanitize('title').escape(); ? ? req.sanitize('author').escape(); ? ? req.sanitize('summary').escape(); ? ? req.sanitize('isbn').escape(); req.sanitize('title').trim(); ? ? req.sanitize('author').trim(); ? ? req.sanitize('summary').trim(); ? ? req.sanitize('isbn').trim(); ? ? req.sanitize('genre').escape(); ? ?? ? ? var book = new Book( ? ? ? { title: req.body.title,? ? ? ? ? author: req.body.author,? ? ? ? ? summary: req.body.summary, ? ? ? ? isbn: req.body.isbn, ? ? ? ? genre: (typeof req.body.genre==='undefined') ? [] : req.body.genre.split(","), ? ? ? ? _id:req.params.id //This is required, or a new ID will be assigned! ? ? ? ?}); ? ?? ? ? var errors = req.validationErrors(); ? ? if (errors) { ? ? ? ? // Re-render book with error information ? ? ? ? // Get all authors and genres for form ? ? ? ? async.parallel({ ? ? ? ? ? ? authors: function(callback) { ? ? ? ? ? ? ? ? Author.find(callback); ? ? ? ? ? ? }, ? ? ? ? ? ? genres: function(callback) { ? ? ? ? ? ? ? ? Genre.find(callback); ? ? ? ? ? ? }, ? ? ? ? }, function(err, results) { ? ? ? ? ? ? if (err) { return next(err); } ? ? ? ? ? ?? ? ? ? ? ? ? // Mark our selected genres as checked ? ? ? ? ? ? for (i = 0; i < results.genres.length; i++) { ? ? ? ? ? ? ? ? if (book.genre.indexOf(results.genres[i]._id) > -1) { ? ? ? ? ? ? ? ? ? ? results.genres[i].checked='true'; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? ? ? res.render('book_form', { title: 'Update Book',authors:results.authors, genres:results.genres, book: book, errors: errors }); ? ? ? ? }); ? ? }? ? ? else { ? ? ? ? // Data from form is valid. Update the record. ? ? ? ? Book.findByIdAndUpdate(req.params.id, book, {}, function (err,thebook) { ? ? ? ? ? ? if (err) { return next(err); } ? ? ? ? ? ? ? ?//successful - redirect to book detail page. ? ? ? ? ? ? ? ?res.redirect(thebook.url); ? ? ? ? ? ? }); ? ? } };
這與創(chuàng)建圖書時使用的路徑非常相似。 首先,我們從表單中驗(yàn)證和清理圖書數(shù)據(jù),并使用它來創(chuàng)建一個新的 Book
對象(將其 _id
值設(shè)置為要更新的對象的id)。 如果在驗(yàn)證數(shù)據(jù)時存在錯誤,那么我們重新呈現(xiàn)表單,另外顯示用戶輸入的數(shù)據(jù),錯誤以及類型和作者列表。 如果沒有錯誤,我們調(diào)用 Book.findByIdAndUpdate()
來更新 Book
文檔,然后重定向到它的詳細(xì)頁面。
打開 /views/book_form.pug 并更新作者表單控件設(shè)置為具有下面粗體顯示的條件代碼的部分。
? ? 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}
注意:需要更改此代碼,以便book_form可用于創(chuàng)建和更新圖書對象(如果沒有這一點(diǎn),則在創(chuàng)建圖書對象時, GET
形成)。
打開 book_detail.pug 視圖,并確保在頁面底部有用于刪除和更新圖書的鏈接,如下所示。
? hr ? p ? ? a(href=book.url+'/delete') Delete Book ? p ? ? a(href=book.url+'/update') Update Book
您現(xiàn)在應(yīng)該可以從圖書詳細(xì)信息頁面更新圖書。
運(yùn)行應(yīng)用程序,打開瀏覽器 http:// localhost:3000 / ,選擇所有圖書 em>鏈接,然后選擇一本特定的書。 最后,選擇更新圖書鏈接。
表單應(yīng)該像創(chuàng)建圖書頁面,只有標(biāo)題為"更新書籍",并且預(yù)先填充了記錄值。
margin:0px auto; width:1000px;">
注意:用于更新對象的其他頁面可以以大致相同的方式實(shí)施。 我們已經(jīng)離開了這是一個挑戰(zhàn)。
實(shí)現(xiàn) Book
, BookInstance
和 Genre
模型的刪除頁面,以相同的方式將它們從相關(guān)聯(lián)的詳細(xì)信息頁面鏈接 >作者刪除頁面。 頁面應(yīng)該遵循相同的設(shè)計方法:
幾個提示:
Genre
is just like deleting an Author
as both objects are dependencies of Books (so in both cases you can only delete the object when the associated books are deleted.Book
is also similar, but you need to check that there are no associated BookInstances
.BookInstance
is the easiest of all, because there are no dependent objects. In this case you can just find the associated record and delete it.實(shí)現(xiàn) BookInstance
,作者
和類型
模型的更新頁面,以與我們的 >預(yù)訂更新頁面。
幾個提示:
Author
date of death and date of birth fields, and the BookInstance
due_date field are the wrong format to input into the date input field on the form (it requires data in form "YYYY-MM-DD"). The easiest way to get around this is to define a new virtual property for the dates that formats the dates appropriately, and then use this field in the associated view templates.NPM上的 Express ,節(jié)點(diǎn)和第三方軟件包提供您在網(wǎng)站中添加表單所需的一切。 在本文中,您學(xué)習(xí)了如何使用 Pug 創(chuàng)建表單,使用 express-validator 驗(yàn)證和清理輸入,以及添加,刪除和修改數(shù)據(jù)庫中的記錄。
您現(xiàn)在應(yīng)該了解如何向您自己的節(jié)點(diǎn)網(wǎng)站添加基本表單和表單處理代碼!
更多建議: