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
-

This is a header component with the title: <%= properties[:title] %>

-

And subtitle <%= properties[:subtitle] %>

+

This is a header component with the title: <%= title %>

+

And subtitle <%= subtitle %>

+ <% if show_links? %> + + <% end %>
``` +```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 %>

- <%= properties[:title] %> + <%= title %>

- <%- 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