diff --git a/README.md b/README.md
index 5445fb1..2c2841a 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@ app/
header.css
header.js
header.yml
+ header_component.rb # optional
```
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.
```erb
```
+```ruby
+# app/components/header/header_component.rb
+class HeaderComponent < MountainView::Presenter
+ properties :title, :subtitle
+ property :links, default: []
+
+ def title
+ properties[:title].titleize
+ end
+
+ def show_links?
+ links.any?
+ end
+end
+```
+
+Including a component class is optional, but it helps avoid polluting your
+views and helpers with presenter logic. Public methods in your component class
+will be made available to the view, along with any properties you define.
+You can also access all properties using the `properties` method in your
+component class and views. You can even define property defaults.
+
### Using components on your views
You can then call your components on any view by using the following
helper:
diff --git a/app/helpers/mountain_view/component_helper.rb b/app/helpers/mountain_view/component_helper.rb
index ae93011..5324451 100644
--- a/app/helpers/mountain_view/component_helper.rb
+++ b/app/helpers/mountain_view/component_helper.rb
@@ -1,7 +1,8 @@
module MountainView
module ComponentHelper
def render_component(slug, properties = {})
- render "#{slug}/#{slug}", properties: properties
+ component = MountainView::Presenter.component_for(slug, properties)
+ component.render(controller.view_context)
end
end
end
diff --git a/lib/mountain_view.rb b/lib/mountain_view.rb
index 2a371f1..45cdf28 100644
--- a/lib/mountain_view.rb
+++ b/lib/mountain_view.rb
@@ -1,5 +1,7 @@
require "mountain_view/version"
require "mountain_view/configuration"
+require "mountain_view/presenter"
+require "mountain_view/component"
module MountainView
def self.configuration
diff --git a/lib/mountain_view/engine.rb b/lib/mountain_view/engine.rb
index 9b8f8e5..c1faea8 100644
--- a/lib/mountain_view/engine.rb
+++ b/lib/mountain_view/engine.rb
@@ -1,5 +1,4 @@
require "rails"
-require "mountain_view/component"
module MountainView
class Engine < ::Rails::Engine
@@ -11,6 +10,12 @@ class Engine < ::Rails::Engine
end
end
+ initializer "mountain_view.load_component_classes",
+ before: :set_autoload_paths do |app|
+ component_paths = "#{MountainView.configuration.components_path}/{*}"
+ app.config.autoload_paths += Dir[component_paths]
+ end
+
initializer "mountain_view.assets" do |app|
Rails.application.config.assets.paths <<
MountainView.configuration.components_path
diff --git a/lib/mountain_view/presenter.rb b/lib/mountain_view/presenter.rb
new file mode 100644
index 0000000..b05db35
--- /dev/null
+++ b/lib/mountain_view/presenter.rb
@@ -0,0 +1,77 @@
+module MountainView
+ class Presenter
+ class_attribute :_properties, instance_accessor: false
+ self._properties = {}
+
+ attr_reader :slug, :properties
+
+ def initialize(slug, properties = {})
+ @slug = slug
+ @properties = default_properties.deep_merge(properties)
+ end
+
+ def render(context)
+ context.extend ViewContext
+ context.inject_component_context self
+ context.render partial: partial
+ end
+
+ def partial
+ "#{slug}/#{slug}"
+ end
+
+ private
+
+ def default_properties
+ self.class._properties.inject({}) do |sum, (k, v)|
+ sum[k] = v[:default]
+ sum
+ end
+ end
+
+ class << self
+ def component_for(*args)
+ klass = "#{args.first.to_s.camelize}Component".safe_constantize
+ klass ||= self
+ klass.new(*args)
+ end
+
+ def properties(*args)
+ opts = args.extract_options!
+ properties = args.inject({}) do |sum, name|
+ sum[name] = opts
+ sum
+ end
+ define_property_methods(args)
+ self._properties = _properties.merge(properties)
+ end
+ alias_method :property, :properties
+
+ private
+
+ def define_property_methods(names = [])
+ names.each do |name|
+ next if method_defined?(name)
+ define_method name do
+ properties[name.to_sym]
+ end
+ end
+ end
+ end
+
+ module ViewContext
+ attr_reader :_component
+ delegate :properties, to: :_component
+
+ def inject_component_context(component)
+ @_component = component
+ protected_methods = MountainView::Presenter.public_methods(false)
+ methods = component.public_methods(false) - protected_methods
+ methods.each do |meth|
+ next if self.class.method_defined?(meth)
+ self.class.delegate meth, to: :_component
+ end
+ end
+ end
+ end
+end
diff --git a/test/dummy/app/components/card/_card.html.erb b/test/dummy/app/components/card/_card.html.erb
index e5073c0..aa00439 100644
--- a/test/dummy/app/components/card/_card.html.erb
+++ b/test/dummy/app/components/card/_card.html.erb
@@ -7,11 +7,12 @@
<% end %>
- <%- if properties[:description] %>
+ <%- if has_description? %>
<%= properties[:description] %>
<%- end %>
+
Location: <%= location %>
<%- if properties[:data] && properties[:data].any? %>
<%- properties[:data].each do |data| %>
@@ -23,4 +24,4 @@
<% end %>
-
\ No newline at end of file
+
diff --git a/test/dummy/app/components/card/card.yml b/test/dummy/app/components/card/card.yml
index b4d4b14..21222f0 100644
--- a/test/dummy/app/components/card/card.yml
+++ b/test/dummy/app/components/card/card.yml
@@ -1,5 +1,6 @@
-
- :title: "Aspen Snowmass"
+ :title: "Snowmass"
+ :location: "Aspen"
: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."
:link: "http://google.com"
:image_url: "http://i.imgur.com/QzuIJTo.jpg"
@@ -13,7 +14,8 @@
-
- :title: "Breckenridge, Colorado"
+ :title: "Breckenridge"
+ :location: "Colorado"
:link: "http://facebook.com"
:image_url: "http://i.imgur.com/w7ZyWPg.jpg"
:data:
diff --git a/test/dummy/app/components/card/card_component.rb b/test/dummy/app/components/card/card_component.rb
new file mode 100644
index 0000000..fa9742c
--- /dev/null
+++ b/test/dummy/app/components/card/card_component.rb
@@ -0,0 +1,14 @@
+class CardComponent < MountainView::Presenter
+ include ActionView::Helpers::TagHelper
+
+ properties :title, :description, :link, :image_url, :location
+ property :data, default: []
+
+ def title
+ [location, properties[:title]].compact.join(", ")
+ end
+
+ def has_description?
+ description.present?
+ end
+end
diff --git a/test/mountain_view/presenter_test.rb b/test/mountain_view/presenter_test.rb
new file mode 100644
index 0000000..fd7bb6a
--- /dev/null
+++ b/test/mountain_view/presenter_test.rb
@@ -0,0 +1,38 @@
+require "test_helper"
+
+class InheritedPresenter < MountainView::Presenter
+ properties :title, :description
+ property :data, default: []
+
+ def title
+ "Foo#{properties[:title].downcase}"
+ end
+end
+
+class MountainView::PresenterTest < ActiveSupport::TestCase
+ test "returns the correct partial path" do
+ presenter = MountainView::Presenter.new("header")
+ assert_equal "header/header", presenter.partial
+ end
+
+ test "exposes properties as provided" do
+ properties = {foo: "bar", hello: "world"}
+ presenter = MountainView::Presenter.new("header", properties)
+ assert_equal properties, presenter.properties
+ end
+
+ test "inherited presenter returns the correct title" do
+ presenter = InheritedPresenter.new("inherited", title: "Bar")
+ assert_equal "Foobar", presenter.title
+ end
+
+ test "inherited presenter responds to #data" do
+ presenter = InheritedPresenter.new("inherited", data: ["Foobar"])
+ assert_equal ["Foobar"], presenter.data
+ end
+
+ test "inherited presenter returns the default value for #data" do
+ presenter = InheritedPresenter.new("inherited", {})
+ assert_equal [], presenter.data
+ end
+end