编辑缩略语页面 封面
Vapor 大型教程

编辑缩略语页面

本章学习如何给缩略语做「编辑」功能:用同一个表单页显示已有数据,提交后把改动写回数据库。

为什么用 POST 而不是 PUT

浏览器原生表单只能发送 GETPOST 请求,无法直接发送 PUT 更新请求。因此我们的「编辑」也走 POST 路由,提交后再在服务器端更新模型。

上下文与路由

WebsiteController.swift 中定义 EditAcronymContext,相比创建上下文多了一个 acronymediting = true 标记,模板借此区分「新建 / 编辑」状态。

struct EditAcronymContext: Encodable {
    let title = "Edit Acronym"
    let acronym: Acronym
    let users: Future<[User]>
    let editing = true
}

注册两条路由:显示编辑页用 GET,提交用 POST,两者都带 Acronym.parameter 以定位要编辑的对象。

router.get("acronyms", Acronym.parameter, "edit", use: editAcronymHandler)
router.post("acronyms", Acronym.parameter, "edit",  use: editAcronymPostHandler)

显示编辑页面

editAcronymHandler 先用 req.parameters.next(Acronym.self) 取出目标缩略语,再用 flatMap(to: View.self) 在拿到结果后查询全部用户,最后渲染与创建页共用的 createAcronym 模板(靠 editing 标记填充旧值)。

func editAcronymHandler(_ req: Request) throws -> Future<View> {
    return try req.parameters.next(Acronym.self)
        .flatMap(to: View.self) { acronym in
            let context = EditAcronymContext(acronym: acronym, users: User.query(on: req).all())
            return try req.view().render("createAcronym", context)
    }
}

提交并保存修改

提交处理方法用 flatMap(to: Response.self, req.parameters.next(Acronym.self), req.content.decode(Acronym.self)) 同时拿到「数据库里的旧对象」和「表单解码出的新数据」,把新值赋给旧对象后保存,再重定向回详情页。

func editAcronymPostHandler(_ req: Request) throws -> Future<Response> {
    return try flatMap(to: Response.self,
                       req.parameters.next(Acronym.self),
                       req.content.decode(Acronym.self)
    ) { acronym, data in
        acronym.short = data.short
        acronym.long = data.long
        acronym.userID = data.userID

        return acronym.save(on: req).map(to: Response.self) { savedAcronym in
            guard let id = savedAcronym.id else {
                throw Abort(HTTPResponseStatus.internalServerError)
            }
            return req.redirect(to: "/acronyms/\(id)")
        }
    }
}

模板与详情页

编辑页复用 createAcronym.leaf,通过 #if(editing) 给输入框填入旧值、下拉框标记选中项,并把按钮文案在 UpdateSubmit 间切换。

#set("content"){
    <h1>#(title)</h1>
    <form method = "post">
        <div class="form-group">
            <label for = "short">Acronym</label>
            <input type="text" name="short" class="form-control" id = "short" #if(editing) { value = "#(acronym.short)" }/>
        </div>
        <div class="form-group">
            <label for = "long">Meaning</label>
            <input type="text" name="long" class="form-control" id="long" #if(editing) { value = "#(acronym.long)"}/>
        </div>
        <div class="form-group">
            <label for="userID">User</label>
            <select name="userID" class="form-control" id="userID">
                #for(user in users) {
                    <option value="#(user.id)" #if(editing){ #if(acronym.userID == user.id) { selected }}>
                        #(user.name)
                    </option>
                }
            </select>
        </div>
        <button type="submit" class = "btn btn-primary">
            #if(editing) { Update } else { Submit }
        </button>
    </form>
}

#embed("base")

详情页 acronym.leaf 提供一个指向编辑页的链接。

#set("content"){
    <h1>#(acronym.short)</h1>
    <h2>#(acronym.long)</h2>
    <p>Created by <a href="/users/#(user.id)/">#(user.name)</a></p>
    <a class="btn btn-primary" href="/acronyms/#(acronym.id)/edit" role="button">Edit</a>
}

#embed("base")
提示:req.content.decode(Acronym.self)req.parameters.next(Acronym.self) 的配合,是 Vapor 3 中「先取旧值、再合并新值」的常见写法。

本系列其他文章