Turbo Rails Tutorial-第八章
Two ways to handle empty states with Hotwire
本章节中,我们将会学习两种方式去使用Turbo控制空状态页,第一种使用Turbo Frames and Turbo Streams
,第二种使用 the only-child CSS pseudo-class.
Adding empty states to our Ruby on Rails applications
空状态页是我们系统中最重要的一部分,当我们第一次访问页面时,页面上没有任何提示让我们知道这个页面能干嘛。所以如果当一个新用户访问我们系统时,显示一点儿图片或者几句话可以更好的表达页面可以操作什么。
如果我们删除了所有的quotes数据,我们的页面也就只剩下标题和按钮了,所以当数据为空时,使用空状态页也会是一个好的选择。让我们开始吧
Empty states with Turbo Frames and Turbo Streams
在敲代码之前,让我们花点儿时间,用草图描述一下将要干嘛。当一个用户没有数据时,我们想展示包含提示信息的空状态
- 如果用户点击header中的
New quote
按钮,Turbo会使用Quotes#new
页面中的frame替换Quotes#index
页面中,id为new_quote
的frame。 - 当用户点击空状态中的
Add quote
按钮时,Turbo一样会使用Quotes#new
页面中的数据替换Quotes#index
的frame
如上面所讲,不管用户点击哪个链接,空状态都会被new quote form
替换掉,页面的状态如下图展示:
当用户提交表单数据,行为也不会发生改变
- 新创建的数据放到表单最前面
- 嵌套id为
new_quote
的frame的HTML被删除
如下图所示
如果你刷新页面,只要至少有一条数据,空状态页就不会再显示
现在我们需求明确了,开始敲代码吧,第一件事儿就是在页面中没有数据时,展示空状态页,为此,我们创建一个空状态的局部视图,并用在Quotes#index
页面
<%# app/views/quotes/_empty_state.html.erb %>
<div class="empty-state">
<p class="empty-state__text">
You don't have any quotes yet!
</p>
<%= link_to "Add quote", new_quote_path, class: "btn btn--primary" %>
</div>
现在我们可以在用户没有数据时,渲染空状态页到Quotes#index
页面中
<%# app/views/quotes/index.html.erb %>
<%= turbo_stream_from current_company, "quotes" %>
<div class="container">
<div class="header">
<h1>Quotes</h1>
<%= link_to "New quote",
new_quote_path,
class: "btn btn--primary",
data: { turbo_frame: dom_id(Quote.new) } %>
</div>
<%= turbo_frame_tag Quote.new do %>
<% if @quotes.none? %>
<%= render "quotes/empty_state" %>
<% end %>
<% end %>
<%= turbo_frame_tag "quotes" do %>
<%= render @quotes %>
<% end %>
</div>
在浏览器测试之前,我们来给空状态页增加点儿样式,让更美观一点儿
// app/assets/stylesheets/components/_empty_state.scss
.empty-state {
padding: var(--space-m);
border: var(--border);
border-style: dashed;
text-align: center;
&__text {
font-size: var(--font-size-l);
color: var(--color-text-header);
margin-bottom: var(--space-l);
font-weight: bold;
}
}
别忘了导入到 manifest 文件中
// app/assets/stylesheets/application.sass.scss
// All the previous code
@import "components/empty_state";
现在就可以测试了。
在Quotes#index
页面中,我们先删除所有的数据,当我们点击New quote or Add quote
按钮时,我们可以看到创建表单替换了空状态页,如果我们提交正常数据,新数据就会被放到最上面,空状态页也不再显示。
然后我们仍然需要做一些改进,如果我们删除了刚刚创建的数据,空状态页并没有回到屏幕上,我们希望只要没有数据了,就显示空状态页,为此我们需要修改destory.turbo_stream.erb
去更改id为new_quote
的frame的内容。
<%# app/views/quotes/destroy.turbo_stream.erb %>
<%= turbo_stream.remove @quote %>
<%= render_turbo_stream_flash_messages %>
<% unless current_company.quotes.exists? %>
<%= turbo_stream.update Quote.new do %>
<%= render "quotes/empty_state" %>
<% end %>
<% end %>
现在,一切如期运转。
然而,我们当前的实现方案,有一个很容易被忽略的问题,在第五章,第六章,我们使用Turbo Stream去订阅:创建,修改,删除的信息通道在Quotes#index
页面,因此,当我还是空状态页时,如果这时候有人创建了一条数据,我这里是可以看到,但是空状态页仍然保留在页面上。
让我们讨论一下问题为什么发生,并如何去解决
Empty states with the only-child CSS pseudo-class
在讨论使用Turbo第二种控制空状态页的方式前,我们再来把问题复现一下,我们来到Quotes#index
页面,并删除所有数据,然后我们在控制台中创建一个新的数据,然后新数据会被广播到浏览器页面中,然后我们就会发现空状态页和数据会同时显示在页面上。
注意:我们这里的例子可能比较繁琐,但让我们花点儿时间想象一下数据被通知,就像Github里的通知一样。
- 当我们页面中没有数据通知时,我们希望展示空状态页
- 当我们通知到达页面时,我们想让空状态页消失
- 当我们删除通知数据时,我们希望空状态页再次返回
这就是这部分我们要完成的任务,并且通知是一个很好的例子
让我们通过第五章,第六章来分析一下这里的问题,在Quote
模型中使用broadcasts_to
方法
- 当数据创建时,
quotes/_quote.html.erb
视图中的内容被渲染到列表前面 - 当数据删除时,这条数据从列表中删除
默认的,并没有对空状态页提及,如果我们修改broadcasts_to
方法的默认选项,使用回调和重写,就有一些巧妙的方法来实现我们想要的功能,归功于CSS中的:only-child pseudo-class
,我们在下面陈列想要实现的行为
- 当空状态是
the only child of the quotes list
,我们想让它展示出来 - 当空状态不是
the only child of the quotes list
,我们想让它消失
这次行为与之前第一种实现方式有一些小小的不同,这次我们不会使用创建表单替换空状态页
让我们开始敲代码吧,首先我们需要把quotes/empty_state
局部视图内容放到数据列表中
<%# app/views/quotes/index.html.erb %>
<%= turbo_stream_from current_company, "quotes" %>
<div class="container">
<div class="header">
<h1>Quotes</h1>
<%= link_to "New quote",
new_quote_path,
class: "btn btn--primary",
data: { turbo_frame: dom_id(Quote.new) } %>
</div>
<%= turbo_frame_tag Quote.new %>
<%= turbo_frame_tag "quotes" do %>
<%= render "quotes/empty_state" %>
<%= render @quotes %>
<% end %>
</div>
然后我们在CSS中使用``:only-child pseudo-class
,当空状态页是id为quotes的Turbo Frame中唯一子节点时显示,如果不是,则再隐藏
// app/assets/stylesheets/components/_empty_state.scss
.empty-state {
padding: var(--space-m);
border: var(--border);
border-style: dashed;
text-align: center;
&__text {
font-size: var(--font-size-l);
color: var(--color-text-header);
margin-bottom: var(--space-l);
font-weight: bold;
}
&--only-child {
display: none;
&:only-child {
display: revert;
}
}
}
在这里,我们使用了一个修饰符来支持本章介绍的两种方法,以便使用相同的.empty-state类。
在我们的空状态页视图中,我们需要明确的指定”Add quote”链接到id为new_quote
的Turbo Frame,我们使用data-turbo-frame="new_quote"
<%# app/views/quotes/_empty_state.html.erb %>
<div class="empty-state empty-state--only-child">
<p class="empty-state__text">
You don't have any quotes yet!
</p>
<%= link_to "Add quote",
new_quote_path,
class: "btn btn--primary",
data: { turbo_frame: dom_id(Quote.new) } %>
</div>
然后我们就可以删除destroy.turbo_stream.erb
中的修改内容了,我们不再需要任何自定义行为
<%# app/views/quotes/destroy.turbo_stream.erb %>
<%= turbo_stream.remove @quote %>
<%= render_turbo_stream_flash_messages %>
现在再在浏览器中试试吧
- 当我们列表中拥有数据时,空状态页消失
- 当没有数据时,空状态页展示
这一部分我们只是用了CSS就完成了任务
Wrap up
本章中,我们使用两种方式去控制空状态页
第一种方式,使用Turbo Frames 和 Turbo Stream去提前添加/删除空状态页在Quotes#index
页面,虽然这个方案适合于大多数情况,但在HTML被广播到页面中的情况并不适和
第二种方式,我们使用了``:only-child CSS pseudo-class
的魔力来完成所有的工作,而不需要写自定义的相关代码。
下面的章节,我们将在Quotes#show
页面中完善我们的quote编辑器,再见