-
Notifications
You must be signed in to change notification settings - Fork 4
编写 Tao Component
假设我们已经有一个 todos#index
页面,这个页面的内容是一个 Todo 的列表:
<!-- app/views/todos/index.html.erb -->
<%= render @todos %>
现在我们希望将 Todo 抽象为独立的组件,这样在 _todo.html.erb
这个 partial 模版里面,只需要调用 Todo 组件对应的 view helper:
<!-- app/views/todos/_todo.html.erb -->
<%= tao_todo_item todo %>
首先,我们需要创建服务器端对应的 Component 类:
# app/components/todos/item_componnent.rb
class Todos::ItemComponent < ApplicationComponent
attr_reader :todo
def initialize view, todo, options = {}
super view, options
@todo = todo
end
end
这个 Component 类会帮助我们动态定义对应的 view helper 方法,这个方法的默认命名规则是 Component.tag_name.underscore
,而 tag_name
的定义是:
# TaoOnRails::Components::Base
def self.tag_name
@tag_name ||= "#{self.tag_prefix}-#{self.component_name.to_s.dasherize}"
end
def self.component_name
@component_name ||= self.name.underscore.split('/').map(&:singularize).join('_')
.gsub(/(.+)_component$/, '\1')
.gsub(/^#{Regexp.quote(self.tag_prefix.to_s.underscore)}_(.+)/, '\1')
end
def self.tag_prefix
:tao
end
所以 Todos::ItemComponent
对应的 view helper 名称就是 tao_todo_item
,其中 tao
这个前缀是可以在 ApplicationComponent 或者 ItemComponent 里面重新定义的,比如:
class Todos::ItemComponent < ApplicationComponent
...
def self.tag_prefix
:awesome
end
end
Component 类的另一个作用是决定组件的渲染逻辑,默认的渲染逻辑会尝试在 app/views/components/todos/
文件夹里面查找名称为 _item.html.erb
的 partial 模版,如果找到了就渲染这个模版,如果没有找到就渲染一个默认的 Custom Element 容器,例如:
<tao-todo-item>
<!-- 如果调用 view helper 的时候提供了block,那么 block 的内容会渲染在这里 -->
</tao-todo-item>
现在我们需要在 app/views/components/todos/
文件夹里面创建 _item.html.erb
模版,然后在这里面编写 Todo 组件的 HTML:
<!-- app/views/components/todos/_todo.html.erb -->
<%= content_tag 'tao-todo-item', id: "todo-item-#{component.todo.id}", completed: component.todo.completed do %>
<%= check_box_tag nil, '1', component.todo.completed, class: 'todo-checkbox' %>
<span class="todo-content"><%= component.todo.content %></span>
<% end %>
Component 对象的实例会作为 local variable 传入这个 partial,所以模版里面可以使用 component
实例的任何 public attribute/method。假设我们现在有一个产品需求是,Todo 内容里面的 “#XXX#” 格式会定义任务的 Tag,Tag 需要抽出来渲染为链接,我们可以通过增加两个 public method 来实现这个需求:
# app/components/todos/item_componnent.rb
class Todos::ItemComponent < ApplicationComponent
...
def tags
/#(.+?)#/.match(todo.content).to_a
end
def content_without_tags
todo.content.gsub(/#(.+?)#/, '')
end
end
然后修改 _todo.html.erb
:
<!-- app/views/components/todos/_todo.html.erb -->
<%= content_tag 'tao-todo-item', id: "todo-item-#{component.todo.id}", completed: component.todo.completed do %>
<%= check_box_tag nil, '1', component.todo.completed, class: 'todo-checkbox' %>
<div class="tags">
<% component.tags.each do |tag| %>
<%= link_to tag, '#', class: 'tag' %>
<% end %>
</div>
<div class="content"><%= component.content_without_tags %></div>
<% end %>
接下来我们要在客户端实现点击完成任务的交互:点击 checkbox 之后,请求完成任务的接口,并且把 Todo 的背景色改为绿色。
为了在点击 checkbox 的之后自动提交 ujs 的 remote 请求,改变服务器上 Todo 的完成状态,我们需要修改一下组件的 partial 模版:
<!-- app/views/components/todos/_todo.html.erb -->
<%= content_tag 'tao-todo-item', id: "todo-item-#{component.todo.id}", completed: component.todo.completed do %>
<%= check_box_tag 'completed', 'true', component.todo.completed, class: 'todo-checkbox',
data: {remote: true, url: todos_complete_path(component.todo), method: 'post'} %>
...
<% end %>
然后在 app/assets/javascripts/todos/components/
文件夹里创建 item.coffee
,编写点击 checkbox 的事件:
class TodoItem extends TaoComponent
@tag 'tao-todo-item'
@attribute 'completed', type: 'boolean', default: false
_connected: ->
@on 'change', '.todo-checkbox', (e) =>
@completed = $(e.currentTarget).is(':checked')
TaoComponent.register TodoItem
其中,_connected
是对 CustomElement 回调方法 connectedCallback
的封装,_connected
会在元素被插入到 DOM 之后被调用, 并且会保证在 DOM ready 之后执行;类似的回调接口还有 _init
_disconnected
_attributeChanged
等,更详细的信息可以参考 API 文档。
TaoComponent.attribute
类方法用来声明组件的 attribute。声明 attribute 可以将组件的实例变量跟 HTML 元素上的 attribute 关联起来:实例变量的 get 方法就是从 HTML 元素上获取对应的 attribute value;set 方法就是将 value 写到 HTML 元素对应的 attribute 里。声明变量的时候还可以指定类型和默认值,这样 get/set 的时候会自动做类型转换和默认值的处理。更详细的信息可以参考 API 文档。
checkbox 被点击之后会设置组件的 completed attribute,这样我们就可以根据这个 attribute 来定义背景色:
// app/assets/stylesheets/todos/components/item.scss
tao-todo-item[completed='true'] {
background: green;
}