forked from nicolas-brousse/view_component
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enable (experimental) single file components (ViewComponent#1556)
* Enable (experimental) single file components This change adds an experimental module, `ViewComponent::InlineTemplate` that allows you to define your templates using a basic DSL directly within your component. This eliminates the need for sidecar templates. This is what it looks like: ```erb class MyComponent < ApplicationComponent include ViewComponent::InlineTemplate template <<~ERB <div> <h1>Hello, <%= @name %>!</h1> </div> ERB def initialize(name:) @name = name end end ``` This results in component being "self-contained", in that a single file can represent the entire component. This makes it easier to understand a component and requires less jumping around between files. This also has benefits like enabling the following, which feels closely related to our multi-template efforts: (this is not implemented, just an example of what this functionality can enable) ```erb class MyComponent < ApplicationComponent include ViewComponent::InlineTemplate template <<~ERB <div> <h1>Hello, <%= @name %>!</h1> <% post.each do.%> <%= render_row(post) %> <% end %> </div> ERB fragment :row, -> (post) { <<~ERB <tr> <td><%= post.title %></td> <td><%= post.body %></td> </tr> ERB } def initialize(name:) @name = name end def posts Post.all end end ``` * Use consistent method names * Add test using slots * add docs * use has_inline_template? method --------- Co-authored-by: Joel Hawksley <[email protected]>
- Loading branch information
Showing
6 changed files
with
267 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# frozen_string_literal: true | ||
|
||
module ViewComponent # :nodoc: | ||
module InlineTemplate | ||
extend ActiveSupport::Concern | ||
Template = Struct.new(:source, :language, :path, :lineno) | ||
|
||
class_methods do | ||
def method_missing(method, *args) | ||
return super if !method.end_with?("_template") | ||
|
||
if defined?(@__vc_inline_template_defined) && @__vc_inline_template_defined | ||
raise ViewComponent::ComponentError, "inline templates can only be defined once per-component" | ||
end | ||
|
||
if args.size != 1 | ||
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" | ||
end | ||
|
||
ext = method.to_s.gsub("_template", "") | ||
template = args.first | ||
|
||
@__vc_inline_template_language = ext | ||
|
||
caller = caller_locations(1..1)[0] | ||
@__vc_inline_template = Template.new( | ||
template, | ||
ext, | ||
caller.absolute_path || caller.path, | ||
caller.lineno | ||
) | ||
|
||
@__vc_inline_template_defined = true | ||
end | ||
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true) | ||
|
||
def respond_to_missing?(method, include_all = false) | ||
method.end_with?("_template") || super | ||
end | ||
|
||
def inline_template | ||
@__vc_inline_template | ||
end | ||
|
||
def inline_template_language | ||
@__vc_inline_template_language if defined?(@__vc_inline_template_language) | ||
end | ||
|
||
def inherited(subclass) | ||
super | ||
subclass.instance_variable_set(:@__vc_inline_template_language, inline_template_language) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
# frozen_string_literal: true | ||
|
||
require "test_helper" | ||
|
||
class InlineErbTest < ViewComponent::TestCase | ||
class InlineErbComponent < ViewComponent::Base | ||
include ViewComponent::InlineTemplate | ||
|
||
attr_reader :name | ||
|
||
erb_template <<~ERB | ||
<h1>Hello, <%= name %>!</h1> | ||
ERB | ||
|
||
def initialize(name) | ||
@name = name | ||
end | ||
end | ||
|
||
class InlineRaiseErbComponent < ViewComponent::Base | ||
include ViewComponent::InlineTemplate | ||
|
||
attr_reader :name | ||
|
||
erb_template <<~ERB | ||
<h1>Hello, <%= raise ArgumentError, "oh no" %>!</h1> | ||
ERB | ||
|
||
def initialize(name) | ||
@name = name | ||
end | ||
end | ||
|
||
class InlineErbSubclassComponent < InlineErbComponent | ||
erb_template <<~ERB | ||
<h1>Hey, <%= name %>!</h1> | ||
ERB | ||
end | ||
|
||
class InlineSlimComponent < ViewComponent::Base | ||
include ViewComponent::InlineTemplate | ||
|
||
attr_reader :name | ||
|
||
slim_template <<~SLIM | ||
h1 | ||
| Hello, | ||
= " " + name | ||
| ! | ||
SLIM | ||
|
||
def initialize(name) | ||
@name = name | ||
end | ||
end | ||
|
||
class InheritedInlineSlimComponent < InlineSlimComponent | ||
end | ||
|
||
class SlotsInlineComponent < ViewComponent::Base | ||
include ViewComponent::InlineTemplate | ||
|
||
renders_one :greeting, InlineErbComponent | ||
|
||
erb_template <<~ERB | ||
<div class="greeting-container"> | ||
<%= greeting %> | ||
</div> | ||
ERB | ||
end | ||
|
||
test "renders inline templates" do | ||
render_inline(InlineErbComponent.new("Fox Mulder")) | ||
|
||
assert_selector("h1", text: "Hello, Fox Mulder!") | ||
end | ||
|
||
test "error backtrace locations work" do | ||
error = assert_raises ArgumentError do | ||
render_inline(InlineRaiseErbComponent.new("Fox Mulder")) | ||
end | ||
|
||
assert_match %r{test/sandbox/test/inline_template_test.rb:26}, error.backtrace[0] | ||
end | ||
|
||
test "renders inline slim templates" do | ||
render_inline(InlineSlimComponent.new("Fox Mulder")) | ||
|
||
assert_selector("h1", text: "Hello, Fox Mulder!") | ||
end | ||
|
||
test "inherits template_language" do | ||
assert_equal "slim", InheritedInlineSlimComponent.inline_template_language | ||
end | ||
|
||
test "subclassed erb works" do | ||
render_inline(InlineErbSubclassComponent.new("Fox Mulder")) | ||
|
||
assert_selector("h1", text: "Hey, Fox Mulder!") | ||
end | ||
|
||
test "calling template methods multiple times raises an exception" do | ||
error = assert_raises ViewComponent::ComponentError do | ||
Class.new(InlineErbComponent) do | ||
erb_template "foo" | ||
erb_template "bar" | ||
end | ||
end | ||
|
||
assert_equal "inline templates can only be defined once per-component", error.message | ||
end | ||
|
||
test "calling template methods with more or less than 1 argument raises" do | ||
assert_raises ArgumentError do | ||
Class.new(InlineErbComponent) do | ||
erb_template | ||
end | ||
end | ||
|
||
assert_raises ArgumentError do | ||
Class.new(InlineErbComponent) do | ||
erb_template "omg", "wow" | ||
end | ||
end | ||
end | ||
|
||
test "works with slots" do | ||
render_inline SlotsInlineComponent.new do |c| | ||
c.with_greeting("Fox Mulder") | ||
end | ||
|
||
assert_selector(".greeting-container h1", text: "Hello, Fox Mulder!") | ||
end | ||
end |