Another CRUD controller with Turbo Rails

本章节中,我们将构建quotes中的日期数据的增删改查,这是一个很好的机会去练习我们之前学习的内容。

What we will build in the following three chapters

现在用户可以创建,修改,删除quotes数据,是时候让我们的quotes编辑器做一些更有用的事儿。

接下来三章,我们将在Quotes#show页面中构建,当本章结束时,我们的用户可以为每个quotes添加多个日期,而每个日期里有多个条目,每个条目拥有名称,可选的描述信息,数量,单价。

在动手之前,我们可以再在线上实例中体验一下:quote editor on hotrails.dev,创建一个quote,进入Quotes#show页面,我们可以创建多个日期,并在日期里添加多个条目,当我们创建,修改,删除条目时,quote的总额也会被更新。

What we will build in this chapter

我们还是在不使用Turbo Frames和Turbo Streams的前提下构建日期数据的增删改查,因为我们需要我们的控制器在进行任何改进之前先能正常工作

让我们画一个草图,当我们访问Quotes#show也页面时,我们应该能看到该条quote的日期数据

image-20230625150959205

当我们不使用Turbo构建增删改查前,点击New date链接时,会带我们到LineItemDates#new页面

image-20230625151127303

当我们提交一个可用数据时,就被重定向到Quotes#show页面,展示新建的数据,而日期数据应该是正序排序好的

image-20230625151311024

如果我们决定修改刚刚创建的数据时,我们可以点击Edit链接,跳转到LineItemDates#edit页面

image-20230625151425060

如果我们提交的是可用数据,则又被重定向到Quotes#show页面中,其中被修改的数据在页面中也更新了。并且数据仍然保证排序

image-20230625151552377

最后当我们想删除数据时,点击Delete链接,数据也就从列表中移除了。

现在需求被明确了,来敲代码吧。

Creating the model

让我们创建LineItemDate模型,拥有日期字段,并包含所属的quote_id字段,每个line item date属于一条quote,而一个quote可以拥有多个line item date。

bin/rails generate model LineItemDate quote:references date:date

在执行rails db:migrate指令前,我们必须给迁移文件添加一些约定

  • 每条LineItemDate的日期字段必须非空,我们会在模型中加入一些校验。
  • 我们希望同一时间一个quote中应该只有一个唯一时间数据,我在数据库层面去控制它,所以会对quote_id and date加入唯一索引
  • 由于我们会对line item date进行排序,为了性能优化,我们也会加上索引。

最终的迁移文件会是这样的:

# db/migrate/XXXXXXXXXXXXXX_create_line_item_dates.rb

class CreateLineItemDates < ActiveRecord::Migration[7.0]
  def change
    create_table :line_item_dates do |t|
      t.references :quote, null: false, foreign_key: true
      # Adding null: false constraint on date
      t.date :date, null: false

      t.timestamps
    end

    # Adding uniqueness constraint for the couple date and quote_id
    add_index :line_item_dates, [:date, :quote_id], unique: true
    # Adding index to the date field for performance reasons
    add_index :line_item_dates, :date
  end
end

执行迁移命令:

bin/rails db:migrate

添加关联关系到LineItemDate模型中,和排序

# app/models/line_item_date.rb

class LineItemDate < ApplicationRecord
  belongs_to :quote

  validates :date, presence: true, uniqueness: { scope: :quote_id }

  scope :ordered, -> { order(date: :asc) }
end
  • 每一天line item date都是非空的,使用presence:true"
  • 一个quote不会有相同的两个date,使用uniqueness: {scope: :quote_id}

再到Quote模型中加入关联关系

# app/models/quote.rb

class Quote < ApplicationRecord
  has_many :line_item_dates, dependent: :destroy

  # All the previous code...
end

我们模型层面就完成了,下面我们再完善路由

Adding routes for line item dates

我们想要执行LineItemDate模型中增删改查中的七个actions,除了下面的两个:

  • 我们不需要LineItemDates#indexaction,因为所有的数据已经在Quotes#show页面中展示了
  • 我们也不需要LineItemDates#showaction,因为展示单条line item date没啥意义。我们需要看quote中所有的数据
# config/routes.rb

Rails.application.routes.draw do
  # All the previous routes

  resources :quotes do
    resources :line_item_dates, except: [:index, :show]
  end
end

我们使用restful resources,看起来很简洁。下一部分,我们会添加一些假的数据给fixtures。

Designing line item dates

Quotes#show页面现在是空的,当我们添加点儿假数据

# test/fixtures/line_item_dates.yml

today:
  quote: first
  date: <%= Date.current %>

next_week:
  quote: first
  date: <%= Date.current + 1.week %>

执行bin/rails db:seed指令后,数据就添加到了数据库,现在打开系统中第一条quote的Quotes#show页面,现在的页面是这样的:

<%# app/views/quotes/show.html.erb %>

<main class="container">
  <%= link_to sanitize("&larr; Back to quotes"), quotes_path %>
  <div class="header">
    <h1>
      <%= @quote.name %>
    </h1>
  </div>
</main>

为了匹配我们的草图, 我们需要一个跳转到LineItemDates#new页面的链接,并且需要渲染line item dates集合。

<%# app/views/quotes/show.html.erb %>

<main class="container">
  <%= link_to sanitize("&larr; Back to quotes"), quotes_path %>

  <div class="header">
    <h1>
      <%= @quote.name %>
    </h1>

    <%= link_to "New date",
                new_quote_line_item_date_path(@quote),
                class: "btn btn--primary" %>
  </div>

  <%= render @line_item_dates, quote: @quote %>
</main>

去渲染line item dates集合,我们首先应该返回所有的line items在QuotesController#showaction中

# app/controllers/quotes_controller.rb

class QuotesController < ApplicationController
  # All the previous code...

  def show
    @line_item_dates = @quote.line_item_dates.ordered
  end

  # All the previous code...
end

接下来我们为单个line item date创建html

<%# app/views/line_item_dates/_line_item_date.html.erb %>

<div class="line-item-date">
  <div class="line-item-date__header">
    <h2 class="line-item-date__title">
      <%= l(line_item_date.date, format: :long) %>
    </h2>

    <div class="line-item-date__actions">
      <%= button_to "Delete",
                    [quote, line_item_date],
                    method: :delete,
                    class: "btn btn--light" %>
      <%= link_to "Edit",
                  [:edit, quote, line_item_date],
                  class: "btn btn--light" %>
    </div>
  </div>
</div>

大多数的标签都被嵌套到class为的.line-item-date_header的div标签中,这是因为我们会在下一章中写.line-item-date_body.line-item-date_footer类,其中将会包含quote中的所有line items和新建line item的链接。为了减少这里要写的CSS/HTML数量,我们之后再处理这里。


注意:我为了更好的可读性,这里使用了不同的路由形式,本教程中我想要让代码更短小一些,如果你还不太熟悉多样的路由形式,下面的两行代码是相同的(但第二种明显更长)

<%= button_to "Delete", [quote, line_item_date] %>
<%= button_to "Delete", quote_line_item_date_path(quote, line_item_date) %>

在Controllers中也可以这么使用,比如下面的代码就是等价的

redirect_to @quote
redirect_to quote_path(@quote)

如果你想了解更多内容,看看文档:the documentation


现在我们已经定义了HTML标签,让我们添加点儿样式吧

// app/assets/stylesheets/components/_line_item_date.scss

.line-item-date {
  margin-top: var(--space-xl);
  margin-bottom: var(--space-xxs);

  &__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-xs);
  }

  &__title {
    font-size: var(--font-size-xl);

    @include media(tabletAndUp) {
      font-size: var(--font-size-xxl);
    }
  }

  &__actions {
    display: flex;
    gap: var(--space-xs);
  }
}

不要忘了引入新的css文件到manifest中

// app/assets/stylesheets/application.sass.scss

// All the previous code
@import "components/line_item_date";

一切正常工作,可以在浏览器中测试一下,现在我们开始写Controller

Our standard CRUD controller

Creating line item dates without Turbo

现在我们的数据库,模型,路由,样式都完成了,是时候开始写Controller了,如上面的介绍,我们先构建一个不使用Turbo Frames and Turb Streams的标准增删改查,我们将在后面再优化。

我们先写#new and #createactions

# app/controllers/line_item_dates_controller.rb

class LineItemDatesController < ApplicationController
  before_action :set_quote

  def new
    @line_item_date = @quote.line_item_dates.build
  end

  def create
    @line_item_date = @quote.line_item_dates.build(line_item_date_params)

    if @line_item_date.save
      redirect_to quote_path(@quote), notice: "Date was successfully created."
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def line_item_date_params
    params.require(:line_item_date).permit(:date)
  end

  def set_quote
    @quote = current_company.quotes.find(params[:quote_id])
  end
end

再加上对应的视图模版

<%# app/views/line_item_dates/new.html.erb %>

<main class="container">
  <%= link_to sanitize("&larr; Back to quote"), quote_path(@quote) %>

  <div class="header">
    <h1>New date</h1>
  </div>

  <%= render "form", quote: @quote, line_item_date: @line_item_date %>
</main>

LineItemDates#new页面不需要特别的设计,因为稍后我们将使用Turbo从页面中提取表单并将其插入Quotes#show页面。但是,对于使用不支持Turbo的旧浏览器的用户来说,它仍然是可用的。让我们为我们的表单添加标记:

<%# app/views/line_item_dates/_form.html.erb %>

<%= simple_form_for [quote, line_item_date], html: { class: "form line-item-date" } do |f| %>
  <% if line_item_date.errors.any? %>
    <div class="error-message">
      <%= line_item_date.errors.full_messages.to_sentence.capitalize %>
    </div>
  <% end %>

  <%= f.input :date, html5: true, input_html: { autofocus: true } %>
  <%= link_to "Cancel", quote_path(quote), class: "btn btn--light" %>
  <%= f.submit class: "btn btn--secondary" %>
<% end %>

现在我们再在浏览器中测试一下吧

Refactoring the error notification message

这是一个好机会去重构表单中提示的异常信息,我们可能已经注意到了不论在quote表单还是line item date表单中使用了相同方式去展示异常数据。

<%# app/views/quotes/_form.html.erb %>

<% if quote.errors.any? %>
  <div class="error-message">
    <%= quote.errors.full_messages.to_sentence.capitalize %>
  </div>
<% end %>
<%# app/views/line_item_dates/_form.html.erb %>

<% if line_item_date.errors.any? %>
  <div class="error-message">
    <%= line_item_date.errors.full_messages.to_sentence.capitalize %>
  </div>
<% end %>

我们想让整个项目一致的展示异常信息,让我们创建helper方法来保证每个表单都使用同样的方式处理

# app/helpers/application_helper.rb

module ApplicationHelper
  # All the previous code

  def form_error_notification(object)
    if object.errors.any?
      tag.div class: "error-message" do
        object.errors.full_messages.to_sentence.capitalize
      end
    end
  end
end

现在,我们的项目有了两个helper方法,现在可以这么做,但当我们的项目越变越大,把这些helper交给逻辑独立的单元管理就很重要了,比如使用FormHelper处理表单相关的代码。不过现在我们不需要着重考虑,我们仅仅消除重复:

<%# app/views/line_item_dates/_form.html.erb %>

<%# All the previous code %>
<%= form_error_notification(line_item_date) %>
<%# All the previous code %>
<%# app/views/quotes/_form.html.erb %>

<%# All the previous code %>
<%= form_error_notification(quote) %>
<%# All the previous code %>

通过这个helper,最终line item date表单就是下面的样子:

<%# app/views/line_item_dates/_form.html.erb %>

<%= simple_form_for [quote, line_item_date], html: { class: "form line-item-date" } do |f| %>
  <%= form_error_notification(line_item_date) %>

  <%= f.input :date, html5: true, input_html: { autofocus: true } %>
  <%= link_to "Cancel", quote_path(quote), class: "btn btn--light" %>
  <%= f.submit class: "btn btn--secondary" %>
<% end %>

记得在浏览器中再测试一下呐

让我们花几秒钟的时间用标签和提交按钮所需的文本填充翻译文件:

# config/locales/simple_form.en.yml

en:
  simple_form:
    placeholders:
      quote:
        name: Name of your quote
    labels:
      quote:
        name: Name
      line_item_date:
        date: Date

  helpers:
    submit:
      quote:
        create: Create quote
        update: Update quote
      line_item_date:
        create: Create date
        update: Update date

这样当创建LineItemDate时就展示:Create date,在修改时展示:Update date

Updating line item dates without Turbo

现在#new and #createaction正常工作了,让我们接着写#edit and #updateaction,我们从Controller开始

class LineItemDatesController < ApplicationController
  before_action :set_quote
  before_action :set_line_item_date, only: [:edit, :update, :destroy]

  # All the previous code

  def edit
  end

  def update
    if @line_item_date.update(line_item_date_params)
      redirect_to quote_path(@quote), notice: "Date was successfully updated."
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private

  def set_line_item_date
    @line_item_date = @quote.line_item_dates.find(params[:id])
  end

  # All the previous code
end

我们知道 #destoryaction 也需要set_line_item_date回调,所以提前加上去了,针对需要这个回调的操作。

现在action已经实现了,让我们添加LineItemDates#edit页面,并在浏览器中测试

<%# app/views/line_item_dates/edit.html.erb %>

<main class="container">
  <%= link_to sanitize("&larr; Back to quote"), quote_path(@quote) %>

  <div class="header">
    <h1>Edit date</h1>
  </div>

  <%= render "form", quote: @quote, line_item_date: @line_item_date %>
</main>

可以看到:LineItemDates#editview 和 LineItemDates#newview 十分相似,只有标题不一样,现在我们在浏览器中测试一下,只剩下一个操作要完成了!

Deleting line item dates without Turbo

这个操作是最简单的,也没有视图,我们只需要在删除后,重定向到Quotes#show页面

class LineItemDatesController < ApplicationController
  # All the previous code

  def destroy
    @line_item_date.destroy

    redirect_to quote_path(@quote), notice: "Date was successfully destroyed."
  end

  # All the previous code
end

一切运转正常,为了防止用户以外删除数据,我们加入提示语

<%# app/views/line_item_dates/_line_item_date.html.erb %>

<!-- All the previous code -->

<%= button_to "Delete",
              quote_line_item_date_path(quote, line_item_date),
              method: :delete,
              form: { data: { turbo_confirm: "Are you sure?" } },
              class: "btn btn--light" %>

<!-- All the previous code -->

button_to helper 生成了HTML中的表单部分,如果我们从DOM中查看,就是下面的样子:

<form data-turbo-confirm="Are you sure?" class="button_to" method="post" action="/quotes/123/line_item_dates/456">
  <input type="hidden" name="_method" value="delete" autocomplete="off">
  <button class="btn btn--light" type="submit">Delete</button>
  <input type="hidden" name="authenticity_token" value="long_token" autocomplete="off">
</form>

需要注意的是data-turbo-confirm数据是在<form> 标签中,但我们删除时,确认信息显示在屏幕上。

现在增删改查就完成了,但我们想要所有的交互都在同一页面中,通过Turbo,我们仅需要几行代码就可以完成优化。

Adding Turbo Frames and Turbo Streams

现在我们让所有的交互操作放到Quotes#show页面,这和我们之前在Quotes#index页面的操作类似

Creating line item dates with Turbo

为了理清需求,让我们画一个草图说明,当用户访问Quotes#show页面,并且点击New date按钮,表单直接渲染到Quotes#show页面,这里就需要Turbo Frames了,当然我们也就需要data-turbo-frame去将New date链接嵌套到一个空的Turbo Frame

image-20230626123613801

为了能进行替换,需要使用相同的id,根据约定,这个id是new_line_item_date,因为它是LineItemDate模型的一个新实例。

image-20230626123817025

当用户点击New date按钮时,Turbo就能成功替换Quotes#show页面中的空Turbo Frame,使用LineItemDates#new页面中Turbo Frame包含的表单。

image-20230626124025866

当用户提交表单后,如果该条quote已经有数据了,我们想要新建的line item被插入到Quotes#show页面中正确的位置上,并按照升序展示。

image-20230626124303741

另一方面,如果该条quote之前没有一条数据,那刚刚新建的数据对应的HTML应该被插入到列表的开始位置。

image-20230626124519572

现在需求已经理清楚了,通过Turbo Frames and Turbo Streams,我们能仅用几行代码就能实现目标。

先来完成第一部分:用户点击New date按钮时,表单出现在Quotes#show页面,这和Quotes#index页面是类似的。在Quotes#show页面中,让我们添加一个id为new_line_itme_date的空Turbo Frame,并且链接到New date按钮上

<%# app/views/quotes/show.html.erb %>

<main class="container">
  <%= link_to sanitize("&larr; Back to quotes"), quotes_path %>

  <div class="header">
    <h1>
      <%= @quote.name %>
    </h1>

    <%= link_to "New date",
                new_quote_line_item_date_path(@quote),
                data: { turbo_frame: dom_id(LineItemDate.new) },
                class: "btn btn--primary" %>
  </div>

  <%= turbo_frame_tag LineItemDate.new %>
  <%= render @line_item_dates, quote: @quote %>
</main>

现在使用相同的Turbo Frame id的标签嵌套我们的表单

<%# app/views/line_item_dates/new.html.erb %>

<main class="container">
  <%= link_to sanitize("&larr; Back to quote"), quote_path(@quote) %>

  <div class="header">
    <h1>New date</h1>
  </div>

  <%= turbo_frame_tag @line_item_date do %>
    <%= render "form", quote: @quote, line_item_date: @line_item_date %>
  <% end %>
</main>

现在当点击新建按钮时,表单就出现在Quotes#show页面了

当我们表单提交异常时,异常信息也以预期方式展示了,这是因为LineItemDatesController在提交异常时,渲染了LineItemDate#new视图,而表单中的Turbo Frame id和渲染页面包含的Turbo Frame拥有相同的new_line_item_dateid,Turbo智能的进行替换,如我们前面章节讨论的,如果表单提交异常,这应该使用unprocessable_entity状态去展示异常数据

当我们表单提交无误时,使用id为new_line_itme_date的Turbo Frame嵌套的表单和重定向响应的Quotes#show页面中的空Turbo Frame拥有相同的ID,所以表单就被空的Turbo Frame替换掉了,但刚刚创建的数据也没有展示啊,这是因为Turbo唔知道我们要怎么处理这部分数据,为了完成我们的需求,我们需要创建一个craete_turbo_stream.erb模板去控制Turbo完成以下行为:

  1. 使用空Turbo Frame替换id为new_line_item_date的turbo frame
  2. 添加新建的line item date到表单中,并放在恰当的位置

第一步和我们之前做的一样,第二步就有点儿复杂了,事实上,如果想要插入新建数据到正确的位置上,那我们必须得知道新建数据在排序队列中应在的位置,让我们看看怎么解决它,首先,编辑LineItemDatesController#createaction去响应到turbo_streamformat中。

# app/controllers/line_item_dates_controller.rb

class LineItemDatesController < ApplicationController
  # All the previous code...

  def create
    @line_item_date = @quote.line_item_dates.build(line_item_date_params)

    if @line_item_date.save
      respond_to do |format|
        format.html { redirect_to quote_path(@quote), notice: "Date was successfully created." }
        format.turbo_stream { flash.now[:notice] = "Date was successfully created." }
      end
    else
      render :new, status: :unprocessable_entity
    end
  end

  # All the previous code...
end

现在让我们想想要实现什么?我们想要插入新建数据到排序列表中正确的位置上,并以从小到大的顺序进行排列,也就是说如果我们的quote已经有了一些数据在新数据的前面,我们新数据就应该放到最后一个,否则,我们应该把数据放到列表的最前面。

在Turbo Stream view里敲代码吧

<%# app/views/line_item_dates/create.turbo_stream.erb %>

<%# Step 1: remove the form from the Quotes#index page %>
<%= turbo_stream.update LineItemDate.new, "" %>

<%# Step 2: add the date at the right place %>
<% if previous_date = @quote.line_item_dates.ordered.where("date < ?", @line_item_date.date).last %>
  <%= turbo_stream.after previous_date do %>
    <%= render @line_item_date, quote: @quote %>
  <% end %>
<% else %>
  <%= turbo_stream.prepend "line_item_dates" do %>
    <%= render @line_item_date, quote: @quote %>
  <% end %>
<% end %>

<%= render_turbo_stream_flash_messages %>

这段代码之后我们就会进行优化,但现在让我们先知道做了什么。

第一步和以前一样,简单的用空Turbo Frame替换form

第二部分就有点儿复杂了,我们先检索新数据前面的数据,如果存在,我们将把新数据加到后面,如果没有,我们将新数据放到队列前面。

为了生效,我们需要把列表嵌套到id为line_item_dates的turbo frame tag中,方便可以把数据加到最前面。

<%# app/views/quotes/show.html.erb %>

<%# All the previous code... %>

<%= turbo_frame_tag "line_item_dates" do %>
  <%= render @line_item_dates, quote: @quote %>
<% end %>

<%# All the previous code... %>

我们也必须包裹每个独立的line item到turbo_frame_tag中,这是因为我们需要通过独立的ID辨别每条独立的Line item,因为我们需要给中间插数据。

<%# app/views/line_item_dates/_line_item_date.html.erb %>

<%= turbo_frame_tag line_item_date do %>
  <div class="line-item-date">
    <!-- All the previous code -->
  </div>
<% end %>

让我们在浏览器中测试一下吧,正常工作了。让我们重构一下之前的代码,并加上一些测试。首先我们需要抽离前面的数据逻辑到LineItemDate模型中。

# app/models/line_item_date.rb

class LineItemDate < ApplicationRecord
  # All the previous code...

  def previous_date
    quote.line_item_dates.ordered.where("date < ?", date).last
  end
end

现在替换视图中这部分逻辑

<%# app/views/line_item_dates/create.turbo_stream.erb %>

<%# Step 1: remove the form from the Quotes#index page %>
<%= turbo_stream.update LineItemDate.new, "" %>

<%# Step 2: add the date at the right place %>
<% if previous_date = @line_item_date.previous_date %>
  <%= turbo_stream.after previous_date do %>
    <%= render @line_item_date, quote: @quote %>
  <% end %>
<% else %>
  <%= turbo_stream.prepend "line_item_dates" do %>
    <%= render @line_item_date, quote: @quote %>
  <% end %>
<% end %>

<%= render_turbo_stream_flash_messages %>

加入测试

# test/models/line_item_date_test.rb

require "test_helper"

class LineItemDateTest < ActiveSupport::TestCase
  test "#previous_date returns the quote's previous date when it exitsts" do
    assert_equal line_item_dates(:today), line_item_dates(:next_week).previous_date
  end

  test "#previous_date returns nil when the quote has no previous date" do
    assert_nil line_item_dates(:today).previous_date
  end
end

这是一个最小的测试了,但能给我们系统正常运转的信心。

工作量不算小啊,#edit,#update and #destoryactions 我们也马上进行晚上。

Updating line item dates with Turbo

#new and #createactions一样,我们想要#edit and #update也能在Quotes#show页面,而我们已经把Quotes#show页面的数据用Turbo Frames嵌套了,如下面草图所示:

image-20230627105453133

当点击编辑图中id为line_item_date_3的Turbo Frame时,Turbo会去到LineItemDates#edit页面去找对应的Turbo Frame,像下图

image-20230627105632515

这样就可以用LineItemDates#edit页面中的表单替换掉Quotes#show页面中的数据了。

image-20230627105749302

当提交表单时,我们又遇到了#new and #create操作时遇到的问题,如何插入被修改的数据,到正确的位置上。而我们也会用同样的方式,如create.turbo_stream.erb页面。我们会先删除操作的数据,然后再把他插入到正确的位置。如下面的草图。

image-20230627110423339

Quotes#show页面最终的状态,应该拥有排序后的数据

image-20230627110544390

理清需求了,敲代码吧,工作的第一部分是让编辑表单成功替换到Quotes#show页面,而我们只需要用Turbo Frame嵌套到LineItemDate#edit

<%# app/views/line_item_dates/edit.html.erb %>

<main class="container">
  <%= link_to sanitize("&larr; Back to quote"), quote_path(@quote) %>

  <div class="header">
    <h1>Edit date</h1>
  </div>

  <%= turbo_frame_tag @line_item_date do %>
    <%= render "form", quote: @quote, line_item_date: @line_item_date %>
  <% end %>
</main>

去浏览器测测看吧

如果表单提交异常,也能正常工作,如果表单正常提交,修改数据被成功替换,但并没有把数据放到正确的位置上。为了确保数据在正常的位置上,我们要用Turbo Stream view,就像我们之前操作#create一样,先允许Controller去渲染Turbo Stream view

# app/controllers/line_item_dates_controller.rb

def update
  if @line_item_date.update(line_item_date_params)
    respond_to do |format|
      format.html { redirect_to quote_path(@quote), notice: "Date was successfully updated." }
      format.turbo_stream { flash.now[:notice] = "Date was successfully updated." }
    end
  else
    render :edit, status: :unprocessable_entity
  end
end

现在创建update.turbo_stream.erb,还是和之前一样的逻辑

<%# app/views/line_item_dates/update.turbo_stream.erb %>

<%# Step 1: remove the form %>
<%= turbo_stream.remove @line_item_date %>

<%# Step 2: insert the updated date at the correct position %>
<% if previous_date = @line_item_date.previous_date %>
  <%= turbo_stream.after previous_date do %>
    <%= render @line_item_date, quote: @quote %>
  <% end %>
<% else %>
  <%= turbo_stream.prepend "line_item_dates" do %>
    <%= render @line_item_date, quote: @quote %>
  <% end %>
<% end %>

<%= render_turbo_stream_flash_messages %>

去浏览器试试看吧

Destroying line item dates with Turbo

最后我们来完成删除Line item dates的工作,先让#destory支持Turbo Stream format

# app/controllers/line_item_dates_controller.rb

def destroy
  @line_item_date.destroy

  respond_to do |format|
    format.html { redirect_to quote_path(@quote), notice: "Date was successfully destroyed." }
    format.turbo_stream { flash.now[:notice] = "Date was successfully destroyed." }
  end
end

然后再删除line item date并且渲染flash message

<%# app/views/line_item_dates/destroy.turbo_stream.erb %>

<%= turbo_stream.remove @line_item_date %>
<%= render_turbo_stream_flash_messages %>

去浏览器试试吧

Testing our code with system tests

如果我们有没有添加测试,那工作是不完整的,我们应该总是写一些测试来保证上线前发现问题,并进行修复。

# test/system/line_item_dates_test.rb

require "application_system_test_case"

class LineItemDatesTest < ApplicationSystemTestCase
  setup do
    login_as users(:accountant)

    @quote          = quotes(:first)
    @line_item_date = line_item_dates(:today)

    visit quote_path(@quote)
  end

  test "Creating a new line item date" do
    assert_selector "h1", text: "First quote"

    click_on "New date"
    assert_selector "h1", text: "First quote"
    fill_in "Date", with: Date.current + 1.day

    click_on "Create date"
    assert_text I18n.l(Date.current + 1.day, format: :long)
  end

  test "Updating a line item date" do
    assert_selector "h1", text: "First quote"

    within id: dom_id(@line_item_date) do
      click_on "Edit"
    end

    assert_selector "h1", text: "First quote"

    fill_in "Date", with: Date.current + 1.day
    click_on "Update date"

    assert_text I18n.l(Date.current + 1.day, format: :long)
  end

  test "Destroying a line item date" do
    assert_text I18n.l(Date.current, format: :long)

    accept_confirm do
      within id: dom_id(@line_item_date) do
        click_on "Delete"
      end
    end

    assert_no_text I18n.l(Date.current, format: :long)
  end
end

上面我们加入了测试增删改,针对line item date,我们使用bin/rails test:all指令,来同时运行单元测试和系统测试。

Wrap up

本章节中,我们做了另一个资源的增删改查操作,与之前不同的是,我们需要保证数据的有序性,通过turbo_stream.after来保证正确位置的插入局部模板到DOM中。

另外我们通过

中的data-turbo-confirm参数来弹出弹框,保证不会误删。

下一章是最后一个大章节,我们将讨论 nested Turbo Frames。