ComponentTags::Visitor
WARNING
ComponentTags::Visitor is experimental and a proof of concept. The generated render calls, the attribute mapping, and the class itself may change or be removed without a major version bump. It prints a warning the first time it is instantiated in a process.
Rewrites capitalized tags into render calls, so a component can be written as a tag instead of an ERB expression.
require "herb/engine/component_tags/visitor"
Herb::Engine.new(source, visitors: [Herb::Engine::ComponentTags::Visitor.new])A tag is transformed when its name is CamelCase in every segment. <DIV>, <BR> and <My-Component /> are left alone, since uppercase HTML tags are valid HTML.
How the tag is resolved is decided entirely from the tag name, with no lookup at compile time or at render time:
| Tag | Separator | Resolves to |
|---|---|---|
<Card /> | none | render Card.new |
<Users::Card /> | ::, a constant | render Users::Card.new |
<Users.Card /> | ., a path | render "users/card" |
<Admin.Users.ProfileCard /> | ., a path | render "admin/users/profile_card" |
Dot notation needs the dot_notation_tags parser option for the tag name to parse at all:
Herb::Engine.new(source,
parser_options: { dot_notation_tags: true },
visitors: [Herb::Engine::ComponentTags::Visitor.new],
)Attribute names are converted from kebab-case to snake_case and become keyword arguments:
| Attribute | Becomes | Notes |
|---|---|---|
name="hello" | name: "hello" | Quotes, backslashes and #{} are escaped |
:count="@count" | count: @count | A : prefix is used as Ruby code |
name="<%= @user.name %>" | name: "#{@user.name}" | ERB is interpolated into the string |
disabled | disabled: true | An attribute without a value |
item-id="7" | item_id: "7" |
An attribute whose name isn't a valid keyword argument, such as @click, is skipped, and the first of a repeated attribute wins.
<MyComponent name="hello" :count="@count" item-id="7" />Use `<MyComponent></MyComponent>` instead of self-closing `<MyComponent />` for HTML compatibility. (html-no-self-closing)Attribute name `:count` contains `:`, which is not valid in an HTML attribute name. Use letters, digits, and hyphens. (html-attribute-name-valid-characters)`<MyComponent>` is written like a component, but this template never opts into slots, so the browser renders it as a literal unknown element. Add `<%# herb:slots client %>` to compile it, or lowercase the tag if it is meant as plain HTML. (herb-component-requires-slots)Compiles to the equivalent of:
<%= render MyComponent.new(name: "hello", count: @count, item_id: "7") %>For a partial, the same attributes become locals instead of keyword arguments:
<Users.Card name="hello" :count="@count" />Use `<Users></Users>` instead of self-closing `<Users />` for HTML compatibility. (html-no-self-closing)Attribute name `:count` contains `:`, which is not valid in an HTML attribute name. Use letters, digits, and hyphens. (html-attribute-name-valid-characters)Attribute name `.Card` contains `.`, which is not valid in an HTML attribute name. Use letters, digits, and hyphens. (html-attribute-name-valid-characters)`<Users>` is written like a component, but this template never opts into slots, so the browser renders it as a literal unknown element. Add `<%# herb:slots client %>` to compile it, or lowercase the tag if it is meant as plain HTML. (herb-component-requires-slots)<%= render "users/card", name: "hello", count: @count %>A tag with a body becomes a block, and the body is compiled as normal, so it can contain HTML, ERB, and further components:
<Card title="Hello">`<Card>` is written like a component, but this template never opts into slots, so the browser renders it as a literal unknown element. Add `<%# herb:slots client %>` to compile it, or lowercase the tag if it is meant as plain HTML. (herb-component-requires-slots) <div>Regular HTML</div>
<%= @thing %>
<Button>Nested component</Button>Closing tag name `</Button>` should be lowercase. Use `</button>` instead. (html-tag-name-lowercase)Opening tag name `<Button>` should be lowercase. Use `<button>` instead. (html-tag-name-lowercase)</Card><%= render Card.new(title: "Hello") do %>
<div>Regular HTML</div>
<%= @thing %>
<%= render Button.new do %>Nested component<% end %>
<% end %>A partial with a body is rendered as a layout, so the body reaches the partial through yield:
<Users.Card title="Hello">Body</Users.Card>Closing tag `</Users>` at (1:32) is missing closing `>`. (`UNCLOSED_CLOSE_TAG_ERROR`) (parser-no-errors)<%= render layout: "users/card", locals: { title: "Hello" } do %>Body<% end %>