Skip to content

Commit 3038ade

Browse files
committed
Merge pull request #39 from tombeynon/component-class
Introduced component presenter classes
2 parents 76d9516 + 36c7d12 commit 3038ade

File tree

9 files changed

+179
-9
lines changed

9 files changed

+179
-9
lines changed

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ app/
4646
header.css
4747
header.js
4848
header.yml
49+
header_component.rb # optional
4950
```
5051

5152
Keep in mind that you can also use `scss`, `coffeescript`, `haml`, or any other
@@ -60,11 +61,40 @@ coffee-script as long as you have these preprocessors running on your app.
6061
```erb
6162
<!-- app/components/header/_header.html.erb -->
6263
<div class="header">
63-
<h1>This is a header component with the title: <%= properties[:title] %></h1>
64-
<h3>And subtitle <%= properties[:subtitle] %></h3>
64+
<h1>This is a header component with the title: <%= title %></h1>
65+
<h3>And subtitle <%= subtitle %></h3>
66+
<% if show_links? %>
67+
<ul>
68+
<% links.each do |link| %>
69+
<li><%= link %></li>
70+
<% end %>
71+
</ul>
72+
<% end %>
6573
</div>
6674
```
6775

76+
```ruby
77+
# app/components/header/header_component.rb
78+
class HeaderComponent < MountainView::Presenter
79+
properties :title, :subtitle
80+
property :links, default: []
81+
82+
def title
83+
properties[:title].titleize
84+
end
85+
86+
def show_links?
87+
links.any?
88+
end
89+
end
90+
```
91+
92+
Including a component class is optional, but it helps avoid polluting your
93+
views and helpers with presenter logic. Public methods in your component class
94+
will be made available to the view, along with any properties you define.
95+
You can also access all properties using the `properties` method in your
96+
component class and views. You can even define property defaults.
97+
6898
### Using components on your views
6999
You can then call your components on any view by using the following
70100
helper:
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
module MountainView
22
module ComponentHelper
33
def render_component(slug, properties = {})
4-
render "#{slug}/#{slug}", properties: properties
4+
component = MountainView::Presenter.component_for(slug, properties)
5+
component.render(controller.view_context)
56
end
67
end
78
end

lib/mountain_view.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
require "mountain_view/version"
22
require "mountain_view/configuration"
3+
require "mountain_view/presenter"
4+
require "mountain_view/component"
35

46
module MountainView
57
def self.configuration

lib/mountain_view/engine.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
require "rails"
2-
require "mountain_view/component"
32

43
module MountainView
54
class Engine < ::Rails::Engine
@@ -11,6 +10,12 @@ class Engine < ::Rails::Engine
1110
end
1211
end
1312

13+
initializer "mountain_view.load_component_classes",
14+
before: :set_autoload_paths do |app|
15+
component_paths = "#{MountainView.configuration.components_path}/{*}"
16+
app.config.autoload_paths += Dir[component_paths]
17+
end
18+
1419
initializer "mountain_view.assets" do |app|
1520
Rails.application.config.assets.paths <<
1621
MountainView.configuration.components_path

lib/mountain_view/presenter.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
module MountainView
2+
class Presenter
3+
class_attribute :_properties, instance_accessor: false
4+
self._properties = {}
5+
6+
attr_reader :slug, :properties
7+
8+
def initialize(slug, properties = {})
9+
@slug = slug
10+
@properties = default_properties.deep_merge(properties)
11+
end
12+
13+
def render(context)
14+
context.extend ViewContext
15+
context.inject_component_context self
16+
context.render partial: partial
17+
end
18+
19+
def partial
20+
"#{slug}/#{slug}"
21+
end
22+
23+
private
24+
25+
def default_properties
26+
self.class._properties.inject({}) do |sum, (k, v)|
27+
sum[k] = v[:default]
28+
sum
29+
end
30+
end
31+
32+
class << self
33+
def component_for(*args)
34+
klass = "#{args.first.to_s.camelize}Component".safe_constantize
35+
klass ||= self
36+
klass.new(*args)
37+
end
38+
39+
def properties(*args)
40+
opts = args.extract_options!
41+
properties = args.inject({}) do |sum, name|
42+
sum[name] = opts
43+
sum
44+
end
45+
define_property_methods(args)
46+
self._properties = _properties.merge(properties)
47+
end
48+
alias_method :property, :properties
49+
50+
private
51+
52+
def define_property_methods(names = [])
53+
names.each do |name|
54+
next if method_defined?(name)
55+
define_method name do
56+
properties[name.to_sym]
57+
end
58+
end
59+
end
60+
end
61+
62+
module ViewContext
63+
attr_reader :_component
64+
delegate :properties, to: :_component
65+
66+
def inject_component_context(component)
67+
@_component = component
68+
protected_methods = MountainView::Presenter.public_methods(false)
69+
methods = component.public_methods(false) - protected_methods
70+
methods.each do |meth|
71+
next if self.class.method_defined?(meth)
72+
self.class.delegate meth, to: :_component
73+
end
74+
end
75+
end
76+
end
77+
end

test/dummy/app/components/card/_card.html.erb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
<% end %>
88
<div class="card__content">
99
<h3 class="card__content__title">
10-
<a href="<%= properties[:link] %>"><%= properties[:title] %></a>
10+
<a href="<%= properties[:link] %>"><%= title %></a>
1111
</h3>
12-
<%- if properties[:description] %>
12+
<%- if has_description? %>
1313
<p><%= properties[:description] %></p>
1414
<%- end %>
15+
<p>Location: <%= location %></p>
1516
<div class="card__content__data">
1617
<%- if properties[:data] && properties[:data].any? %>
1718
<%- properties[:data].each do |data| %>
@@ -23,4 +24,4 @@
2324
<% end %>
2425
</div>
2526
</div>
26-
</div>
27+
</div>

test/dummy/app/components/card/card.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
-
2-
:title: "Aspen Snowmass"
2+
:title: "Snowmass"
3+
:location: "Aspen"
34
:description: "Aspen Snowmass is a winter resort complex located in Pitkin County in western Colorado in the United States. Owned and operated by the Aspen Skiing Company it comprises four skiing/snowboarding areas on four adjacent mountains in the vicinity of the towns of Aspen and Snowmass Village."
45
:link: "http://google.com"
56
:image_url: "http://i.imgur.com/QzuIJTo.jpg"
@@ -13,7 +14,8 @@
1314

1415

1516
-
16-
:title: "Breckenridge, Colorado"
17+
:title: "Breckenridge"
18+
:location: "Colorado"
1719
:link: "http://facebook.com"
1820
:image_url: "http://i.imgur.com/w7ZyWPg.jpg"
1921
:data:
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class CardComponent < MountainView::Presenter
2+
include ActionView::Helpers::TagHelper
3+
4+
properties :title, :description, :link, :image_url, :location
5+
property :data, default: []
6+
7+
def title
8+
[location, properties[:title]].compact.join(", ")
9+
end
10+
11+
def has_description?
12+
description.present?
13+
end
14+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require "test_helper"
2+
3+
class InheritedPresenter < MountainView::Presenter
4+
properties :title, :description
5+
property :data, default: []
6+
7+
def title
8+
"Foo#{properties[:title].downcase}"
9+
end
10+
end
11+
12+
class MountainView::PresenterTest < ActiveSupport::TestCase
13+
test "returns the correct partial path" do
14+
presenter = MountainView::Presenter.new("header")
15+
assert_equal "header/header", presenter.partial
16+
end
17+
18+
test "exposes properties as provided" do
19+
properties = { foo: "bar", hello: "world" }
20+
presenter = MountainView::Presenter.new("header", properties)
21+
assert_equal properties, presenter.properties
22+
end
23+
24+
test "inherited presenter returns the correct title" do
25+
presenter = InheritedPresenter.new("inherited", title: "Bar")
26+
assert_equal "Foobar", presenter.title
27+
end
28+
29+
test "inherited presenter responds to #data" do
30+
presenter = InheritedPresenter.new("inherited", data: ["Foobar"])
31+
assert_equal ["Foobar"], presenter.data
32+
end
33+
34+
test "inherited presenter returns the default value for #data" do
35+
presenter = InheritedPresenter.new("inherited", {})
36+
assert_equal [], presenter.data
37+
end
38+
end

0 commit comments

Comments
 (0)