Rails Rails 中实现自定义卡片视图帮助方法的心路历程

yetrun · 2024年07月26日 · 最后由 jiting 回复于 2024年07月26日 · 224 次阅读

实现一个基本的 cards_for 效果

我需要用 BootStrap Card 组件实现一个卡片列表,单个的 BootStrap Card 组件的使用方式如下:

<div class="card" style="width: 18rem;">
  <div class="card-body">
    <h5 class="card-title">Card title</h5>
    <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
    <a href="#" class="btn btn-primary">Go somewhere</a>
  </div>
</div>

这是它的效果:

现在我需要展示一堆这样的卡片,卡片之间要有间距,大致是这样的效果(这是一行的效果,实际上应当支持多行,这里不再展示):

就此机会,我的第一步是实现一个名为 card 的局部视图,像这样:

<!-- _card.html.erb -->
<div class="card" style="width: 18rem;">
  <div class="card-body">
    <h5 class="card-title"><%= name %></h5>
    <p class="card-text"><%= description %></p>
    <a href="<%= link %>" class="btn btn-primary">Go somewhere</a>
  </div>
</div>

card 局部视图接受三个变量:namedescriptionlink,分别显示卡片的标题,正文和指向的链接。

如果需要显示一堆卡片,我需要这样做:

<div class="d-flex gap-3 p-3 flex-wrap">
  <% @models.each do |model| %>
    <%= render "card", name: model.name, 
                       description: model.description, 
                       link: model.link %>
  <% end %>
</div>

然后我想到我的项目中有一堆这样模型,它也是满足 namedescriptionlink 这三个属性的。为了减少重复代码,我决定实现一个自定义的 helper 方法,来简化这些卡片列表的渲染过程。这不仅能提高代码的可读性,还能确保一致的样式和结构。

我将这个方法命名为 cards_for,就像 Rails 自带的视图帮助方法 form_for 等一样,cards_for方法只接受一个满足要求的 models 属性即可:

<%= cards_for models: @models %>

在 Helper 中我只需要实现成这样:

module CardsHelper
  def cards_for(models:)
    cards_html = models.map do |model|
      render "card", name: model.name, 
                     description: model.description, 
                     link: model.link    
    end.join.html_safe
    content_tag :div, cards_html, class: "d-flex gap-3 p-3 flex-wrap"
  end
end

在 Helper 中写代码,简直就像在视图中写代码那样,只要你找到对应的方法。

cards_for 添砖加瓦所受的伤

接下来,我的任务是继续为 cards_for 方法添砖加瓦。因为我们总不能保证传递进来的模型满足 namedescriptionlink 这样的命名关系,为了适应这种情况,我的初步想法是,为 cards_for 方法添加一个块,将块的返回值作为局部变量传递给 card 视图:

module CardsHelper
  def cards_for(models:, &block)
    cards_html = models.map do |model|
      if block_given?
        options = block.call(model)
      else
        options = {
          name: model.name, 
          description: model.description, 
          link: model.link
        }
      end

      render partial: "card", locals: options # 保险起见,我写成这样
    end.join.html_safe
    content_tag :div, cards_html, class: "d-flex gap-3 p-3 flex-wrap"
  end
end

然后我们就可以针对模型显示一些不一样的东西,例如:

<%= cards_for models: @models do |model| %>
  {
    name: "T-" + model.name,
    description: "T-" + model.description,
    link: "https://www.sina.com"
  }
<%  end %>

然后给我报了这样一个错误:

undefined method `keys' for "\n  {\n    name: \"T-\" + model.name,\n    description: \"T-\" + model.description,\n    link: \"https://www.sina.com\"\n  }\n":String

原因是我调用的写法写错了,要改成这样才正确:

<%= cards_for models: @models do |model|
  {
    name: "T-" + model.name,
    description: "T-" + model.description,
    link: "https://www.sina.com"
  }
end %>

注意区别很有限,只比上一个少了一个 %> 结束标签和 <% 打开标签,但也是我们经常犯的错误。原因是因为 ERB 对于不在 <% ... %> 标签内的代码,它会视为纯文本。请细细品味,方能知晓。

至此,一个 cards_for 帮助方法已经实现了。还可以继续为其添砖加瓦,利用 Ruby 语言的灵活性。

jiting 回复

好建议呀!只不过我看不到它的组件?它是一个组件库,还是说是个框架提供了封装组件的基类?

yetrun 回复

只是个框架

需要 登录 后方可回复, 如果你还没有账号请 注册新账号