showcase 0.1.7 → 0.2.0.beta

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 93da90243af0c3fe8b795954fbdf5df12a28007b
4
- data.tar.gz: 463b88936212e9e181cb15fe44613e947ab116cb
3
+ metadata.gz: 3abceb896eb37c725738c997b0bddcb09e74605b
4
+ data.tar.gz: 8ad7e9cd6cc06b41bf2008f1e25a4116eed1e80f
5
5
  SHA512:
6
- metadata.gz: b7a38f5e9d326581b10ea36a15ffd78019a5b3fae9deb475ed6565723e187e3894383ee1ec3c8574784cd3c25e65aeed63f8793891ca8b66db588780371e615a
7
- data.tar.gz: 69ed3a3ac0c0dbc8c7a12f4d1b0fd018a5ffe465ddb611de1ba576e2fbca792e692b09296a93e33e6a90eb63fb447d9821dda374a9122c6e3ba30a838218d059
6
+ metadata.gz: 1ff5470207bd67524b1de59fa28e4bc9970c2488ed9df89392a8630f939e1f0d5bea6445528cbfbf9e9f3c631b4765f001cd4acccd45208c217fbf8978583ba0
7
+ data.tar.gz: 1454964cabb2b9f4be47fee3d041a3f3cdcbe061375a29de485d96bb10ae564c23100ea06643c92bcda3589f692f1ce74e0fa5b3356746b2c2676ad8f4c080fa
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+
2
+
data/.travis.yml CHANGED
@@ -1,4 +1,12 @@
1
1
  language: ruby
2
+
2
3
  rvm:
3
4
  - "1.9.3"
4
- script: "bundle exec rspec spec"
5
+ - "2.0.0"
6
+
7
+ gemfile:
8
+ - "gemfiles/activerecord3.gemfile"
9
+ - "gemfiles/activerecord4.gemfile"
10
+
11
+ script: "bundle exec rake spec"
12
+
data/Appraisals ADDED
@@ -0,0 +1,8 @@
1
+ appraise "activerecord3" do
2
+ gem "activerecord", "3.2.14"
3
+ end
4
+
5
+ appraise "activerecord4" do
6
+ gem "activerecord", "4.0.0"
7
+ end
8
+
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in basic_presenter.gemspec
4
4
  gemspec
5
+
6
+ gem "appraisal"
7
+
data/README.md CHANGED
@@ -1,17 +1,15 @@
1
- # Showcase [![Build Status](https://travis-ci.org/welaika/showcase.png?branch=master)](https://travis-ci.org/welaika/showcase) [![Coverage Status](https://coveralls.io/repos/welaika/showcase/badge.png?branch=master)](https://coveralls.io/r/welaika/showcase)
1
+ # Showcase [![Build Status](https://travis-ci.org/stefanoverna/showcase.png?branch=master)](https://travis-ci.org/stefanoverna/showcase) [![Coverage Status](https://coveralls.io/repos/stefanoverna/showcase/badge.png?branch=master)](https://coveralls.io/r/stefanoverna/showcase)
2
2
 
3
- A simple (< 100 lines of code) but powerful exhibit/presenter implementation. It's framework agnostic: works with Rails, Padrino or simply Sinatra.
3
+ A simple (< 100 lines of code) but powerful exhibit/presenter implementation.
4
+ It's framework agnostic: works with Rails, Padrino or just Sinatra.
4
5
 
5
- ## Properties of the Exhibit pattern
6
+ Since version 0.2.0 Showcase is bundled with [a set of "traits"](https://github.com/stefanoverna/showcase#traits)
7
+ you can pick and choose to augment your presenters with additional sugar
8
+ (available in Rails 3+ only).
6
9
 
7
- Citing [Avdi's introductory post](http://devblog.avdi.org/2012/06/04/displaycase-gem-now-available/):
10
+ ## Why should I use presenters in my Rails app?
8
11
 
9
- * **It wraps a single model instance.** Not an assortment of objects.
10
- * **It is a true Decorator.** All unrecognized messages are passed through to the underlying object. This facilitates a gradual migration to the use of Exhibits to encapsulate presentation knowledge, since they can be layered onto models without any change to the existing views. It also enables multiple Exhibits to be layered onto an object, each handling different aspects of presentation.
11
- * **It brings together a model and a context.** Exhibits need a reference to a “context” object—either a controller or a view context—in order to be able to render templates as well as construct URLs for the object or related resources.
12
- * **It encapsulates decisions about how to render an object.** The tell-tale of an Exhibit is telling an object “render yourself”, rather than explicitly rendering a template and passing the object in as an argument.
13
- * **It may modify the behavior of an object.** For instance, an Exhibit might impose a scope on a `Blog#entries` association which only returns entries that are visible to the current user (as determined from the Exhibit’s controller context). Or it might reformat the return value of a `#social_security_number` method to include dashes and have all but the last four digits obscured: `***-**-5678`.
14
- * **There is a many-to-many relationship between model classes and exhibit classes.** One generic exhibit class may apply to several different types of model. Other exhibits may be specific, not just to a model, but to a model in a particular state, or within a particular viewing context.
12
+ See [Avdi's Exhibits introductory post](http://devblog.avdi.org/2012/06/04/displaycase-gem-now-available/).
15
13
 
16
14
  ## Installation
17
15
 
@@ -29,15 +27,8 @@ Or install it yourself as:
29
27
 
30
28
  ## Usage
31
29
 
32
- With Rails, include `Showcase::Helpers` into your controller and/or views:
33
-
34
- ```ruby
35
- # config/initializers/showcase.rb
36
- ActionController::Base.send :include, Showcase::Helpers
37
- ActionView::Base.send :include, Showcase::Helpers
38
- ```
39
-
40
- With Padrino, include `Showcase::Helpers` in your app `helpers` block.
30
+ With Rails, you're already set, move on! With Padrino, include `Showcase::Helpers`
31
+ in your app `helpers` block.
41
32
 
42
33
  ```ruby
43
34
  helpers do
@@ -45,7 +36,8 @@ helpers do
45
36
  end
46
37
  ```
47
38
 
48
- You can now instantiate new presenters in your controller/views using the included helpers:
39
+ You can now instantiate new presenters in your controller/views using the
40
+ included helpers:
49
41
 
50
42
  ```ruby
51
43
  # this is the object that needs to be presented
@@ -74,7 +66,8 @@ class ProjectPresenter < Showcase::Presenter
74
66
  # automatically wraps the attribute into an AdminPresenter
75
67
  presents :person, with: AdminPresenter
76
68
 
77
- # expects project.task to return an enumerable. automatically wraps each task in a TaskPresenter presenter
69
+ # expects project.task to return an enumerable. automatically wraps each task
70
+ # in a TaskPresenter presenter
78
71
  presents_collection :tasks
79
72
 
80
73
  # you can use `view_context`, or the shortcut `h`, to access the context.
@@ -85,6 +78,178 @@ class ProjectPresenter < Showcase::Presenter
85
78
  end
86
79
  ```
87
80
 
81
+ ## Rails
82
+
83
+ ### Generators
84
+
85
+ Showcase comes with a generator to create new presenters a little faster:
86
+
87
+ ```
88
+ rails generate showcase:presenter User
89
+ ```
90
+
91
+ Will generate `app/presenters/user_presenter.rb`. If your Rails app has the file
92
+ `app/presenters/base_presenter.rb`, the newly created presenter will inherit
93
+ from `BasePresenter` instead of `Shocase::Presenter`.
94
+
95
+ ### Traits
96
+
97
+ Please [read the tests](https://github.com/stefanoverna/showcase/tree/master/lib/showcase/traits)
98
+ for a detailed explanation of each method available.
99
+
100
+ #### `Showcase::Traits::Record`
101
+
102
+ To be used to present ActiveModel-based records. Inside your presenter, include
103
+ the trait like this:
104
+
105
+ ```ruby
106
+ class ProjectPresenter < Showcase::Presenter
107
+ include Showcase::Traits::Record
108
+ end
109
+ ```
110
+
111
+ ##### `#dom_id`
112
+
113
+ ```ruby
114
+ present(@project).dom_id # => "project_12"
115
+ ```
116
+
117
+ ##### `#dom_class`
118
+
119
+ ```ruby
120
+ present(@project).dom_class # => "project"
121
+ ```
122
+
123
+ ##### `#box`
124
+
125
+ Super useful in acceptance testing to check the presence of a record inside a
126
+ view:
127
+
128
+ ```erb
129
+ <% present(@project).box do %>
130
+ <p>Hi there!</p>
131
+ <% end %>
132
+ ```
133
+
134
+ Produces the following:
135
+
136
+ ```html
137
+ <div class="project" id="project_12">
138
+ <p>Hi there</p>
139
+ </div>
140
+
141
+ Additional HTML attributes can be optionally specified within a config block
142
+ inside the presenter:
143
+
144
+ ```ruby
145
+ class ProjectPresenter < Showcase::Presenter
146
+ include Showcase::Traits::Record
147
+
148
+ box do |c|
149
+ c.html_options class: 'another-class', role: 'project'
150
+ end
151
+ end
152
+ ```
153
+
154
+ #### `Showcase::Traits::LinkTo`
155
+
156
+ Adds a nice DSL to declare links within your presenter.
157
+
158
+ ```ruby
159
+ class ProjectPresenter < Showcase::Presenter
160
+ include Showcase::Traits::LinkTo
161
+
162
+ link_to do |c|
163
+ c.url h.project_path(self)
164
+ c.label name
165
+ c.active h.controller_name == 'projects'
166
+ c.active_class 'current'
167
+ c.html_options role: 'label'
168
+ end
169
+
170
+ link_to :tasks do
171
+ c.url h.project_tasks_path(self)
172
+ c.label "Tasks"
173
+ end
174
+ end
175
+ ```
176
+
177
+ In your views:
178
+
179
+ ```erb
180
+ <%= project.url %>
181
+ <%= project.tasks_url %>
182
+
183
+ <%= project.link_active? %>
184
+ <%= project.tasks_link_active? %>
185
+
186
+ <%= project.link %>
187
+ <%= project.tasks_link %>
188
+
189
+ <%= project.link('Alternative label') %>
190
+ <%= project.link(class: 'additional_class') %>
191
+ <%= project.link do %>
192
+ Link content!
193
+ <% end %>
194
+ ```
195
+
196
+ #### `Showcase::Traits::Share`
197
+
198
+ Useful to produce social share links:
199
+
200
+ ```ruby
201
+ class ProjectPresenter < Showcase::Presenter
202
+ include Showcase::Traits::Share
203
+
204
+ share do |c|
205
+ c.url h.project_path(self)
206
+ c.text name
207
+ c.image_url cover_image
208
+ end
209
+ end
210
+ ```
211
+ In your views:
212
+
213
+ ```erb
214
+ <%= project.twitter_share_url %>
215
+ <%= project.twitter_share_link %>
216
+ <%= project.twitter_share_link('Alternative label') %>
217
+ <%= project.twitter_share_link(class: 'additional_class') %>
218
+ <%= project.twitter_share_link do %>
219
+ Link content!
220
+ <% end %>
221
+
222
+ <%= project.facebook_share_link %>
223
+ <%= project.gplus_share_link %>
224
+ <%= project.pinterest_share_link %>
225
+ <%= project.linkedin_share_link %>
226
+ ```
227
+
228
+ #### `Showcase::Traits::Seo`
229
+
230
+ Useful to produce SEO meta tags (title, description, Facebook OpenGraph,
231
+ Twitter cards, and canonical URL):
232
+
233
+ ```ruby
234
+ class ProjectPresenter < Showcase::Presenter
235
+ include Showcase::Traits::Seo
236
+
237
+ seo do |c|
238
+ c.title name
239
+ c.description [ description, 'Fallback description if blank' ]
240
+ c.image_url cover_thumb_image
241
+ c.canonical_url h.project_path(self)
242
+ end
243
+ end
244
+ ```
245
+ In your views:
246
+
247
+ ```erb
248
+ <% content_for(:head) do %>
249
+ <%= present(@project).seo_tags(title_suffix: ' - BaseClump') %>
250
+ <% end %>
251
+ ```
252
+
88
253
  ## Contributing
89
254
 
90
255
  1. Fork it
data/Rakefile CHANGED
@@ -1 +1,14 @@
1
- require "bundler/gem_tasks"
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'appraisal'
4
+
5
+ desc 'Default: run specs'
6
+ task default: :spec
7
+
8
+ require 'rspec/core/rake_task'
9
+ RSpec::Core::RakeTask.new do |t|
10
+ t.pattern = "spec/**/*_spec.rb"
11
+ end
12
+
13
+ Bundler::GemHelper.install_tasks
14
+
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "activerecord", "3.2.14"
7
+
8
+ gemspec :path=>"../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "activerecord", "4.0.0"
7
+
8
+ gemspec :path=>"../"
@@ -4,8 +4,10 @@ module Showcase
4
4
  source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
5
5
 
6
6
  def create_presenter
7
+ @base_presenter = File.exist?('app/presenters/base_presenter.rb') ? "BasePresenter" : "Showcase::Presenter"
7
8
  template 'presenter.rb', File.join('app/presenters', class_path, "#{file_name}_presenter.rb")
8
9
  end
9
10
  end
10
11
  end
11
12
  end
13
+
@@ -1,4 +1,5 @@
1
1
  <% module_namespacing do -%>
2
- class <%= class_name %>Presenter < Showcase::Presenter
2
+ class <%= class_name %>Presenter < <%= @base_presenter %>
3
3
  end
4
4
  <% end -%>
5
+
@@ -0,0 +1,28 @@
1
+ require 'ostruct'
2
+
3
+ module Showcase
4
+ module Helpers
5
+ class ConfigObject
6
+ def initialize(context, &block)
7
+ @config_block = block
8
+ @context = context
9
+ end
10
+
11
+ def to_struct
12
+ OpenStruct.new(to_hash)
13
+ end
14
+
15
+ def to_hash
16
+ @result = {}
17
+ @context.instance_exec(self, &@config_block)
18
+ @result.symbolize_keys
19
+ end
20
+
21
+ def method_missing(name, *args, &block)
22
+ name = name.to_s.gsub(/=$/, '')
23
+ @result[name.to_sym] = args.first
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,22 @@
1
+ class HtmlOptions
2
+ def initialize(options = {})
3
+ @options = (options || {}).symbolize_keys
4
+ end
5
+
6
+ def add_class!(css_class)
7
+ @options[:class] ||= ""
8
+ css_classes = @options[:class].split(/\s+/)
9
+ css_classes << css_class
10
+ @options[:class] = css_classes.join(" ")
11
+ end
12
+
13
+ def merge_attrs!(options = {})
14
+ options = (options || {}).symbolize_keys
15
+ @options.merge!(options)
16
+ end
17
+
18
+ def to_h
19
+ @options.dup
20
+ end
21
+ end
22
+
@@ -0,0 +1,56 @@
1
+ module Showcase
2
+ module Helpers
3
+ class SeoMetaBuilder
4
+ attr_reader :context
5
+
6
+ def initialize(context)
7
+ @context = context
8
+ end
9
+
10
+ def title(values, options = {})
11
+ title = first_nonblank(values)
12
+ title += options[:title_suffix] if options[:title_suffix]
13
+
14
+ context.content_tag(:title, title) <<
15
+ seo_meta_tags(:og_title, :twitter_title, values)
16
+ end
17
+
18
+ def description(values, options = {})
19
+ seo_meta_tags(:description, :og_description, :twitter_description, values)
20
+ end
21
+
22
+ def image_url(image_url, options = {})
23
+ seo_meta_tags(:og_image, :twitter_image, image_url)
24
+ end
25
+
26
+ def canonical_url(url, options = {})
27
+ seo_meta_tags(:og_url, url) <<
28
+ context.tag(:link, rel: "canonical", "href" => url)
29
+ end
30
+
31
+ private
32
+
33
+ def first_nonblank(values)
34
+ Array(values).map(&:presence).compact.first
35
+ end
36
+
37
+ def seo_meta_tags(*args)
38
+ value = first_nonblank(args.pop)
39
+
40
+ return nil unless value.present?
41
+
42
+ args.map do |name|
43
+ chunks = name.to_s.split("_")
44
+ attr_name = if chunks.first == 'og'
45
+ 'property'
46
+ else
47
+ 'name'
48
+ end
49
+ name = chunks.join(':')
50
+ context.tag(:meta, attr_name => name, content: value)
51
+ end.join.html_safe
52
+ end
53
+ end
54
+ end
55
+ end
56
+
@@ -12,6 +12,7 @@ module Showcase
12
12
  alias_method :object, :__getobj__
13
13
  alias_method :h, :view_context
14
14
  alias_method :try, :__send__
15
+ alias_method :__decorator_class__, :class
15
16
 
16
17
  def initialize(obj, context)
17
18
  super(obj)
@@ -0,0 +1,12 @@
1
+ require 'showcase/traits'
2
+
3
+ module Showcase
4
+ class Railtie < Rails::Railtie
5
+ initializer "action_view.initialize_showcase" do
6
+ ActiveSupport.on_load(:action_view) do
7
+ ActionView::Base.send :include, Showcase::Helpers
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,23 @@
1
+ module Showcase
2
+ module Traits
3
+ module Base
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ private
8
+
9
+ def define_method?(name_chunks, &block)
10
+ method_name = Array(name_chunks).map(&:to_s).map(&:presence).compact.join("_")
11
+ if method_defined?(method_name)
12
+ false
13
+ else
14
+ define_method(method_name, &block)
15
+ true
16
+ end
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,37 @@
1
+ require 'showcase/helpers/config_object'
2
+ require 'showcase/helpers/html_options'
3
+
4
+ module Showcase
5
+ module Traits
6
+
7
+ module LinkTo
8
+ extend ActiveSupport::Concern
9
+ include Base
10
+
11
+ module ClassMethods
12
+ def link_to(name = nil, &block)
13
+ define_method? [name, :url] do
14
+ Helpers::ConfigObject.new(self, &block).to_struct.url
15
+ end
16
+
17
+ define_method? [name, :link_active?] do
18
+ Helpers::ConfigObject.new(self, &block).to_struct.active
19
+ end
20
+
21
+ define_method? [name, :link] do |*args, &link_block|
22
+ config = Helpers::ConfigObject.new(self, &block).to_struct
23
+
24
+ html_options = HtmlOptions.new(config.html_options)
25
+ html_options.merge_attrs!(args.extract_options!)
26
+ html_options.add_class!(config.active_class || 'active') if config.active
27
+
28
+ args = Array(config.label) if args.empty? && !link_block
29
+ h.link_to *args, config.url, html_options.to_h, &link_block
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+
@@ -0,0 +1,61 @@
1
+ module Showcase
2
+ module Traits
3
+
4
+ module Record
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def box(&block)
9
+ @box_config_block = block
10
+ end
11
+
12
+ def __box_config_block__
13
+ @box_config_block
14
+ end
15
+ end
16
+
17
+ def dom_id
18
+ record_identifier.dom_id(self)
19
+ end
20
+
21
+ def dom_class
22
+ record_identifier.dom_class(self)
23
+ end
24
+
25
+ def box(*args, &block)
26
+ options = args.extract_options!
27
+ tag = args.pop || :div
28
+
29
+ config_block = self.__decorator_class__.__box_config_block__
30
+ config_options = if config_block
31
+ Helpers::ConfigObject.new(self, &config_block).to_struct.html_options
32
+ else
33
+ {}
34
+ end
35
+
36
+ html_options = HtmlOptions.new(config_options)
37
+ html_options.merge_attrs!(options)
38
+ html_options.add_class!(dom_class)
39
+ html_options.merge_attrs!(id: dom_id)
40
+
41
+ h.content_tag(tag, html_options.to_h) do
42
+ h.capture(self, &block)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def record_identifier
49
+ if defined?(ActionView::RecordIdentifier)
50
+ ActionView::RecordIdentifier
51
+ elsif defined?(ActionController::RecordIdentifier)
52
+ ActionController::RecordIdentifier
53
+ else
54
+ raise 'No RecordIdentifier found!'
55
+ end
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+
@@ -0,0 +1,27 @@
1
+ require 'showcase/helpers/config_object'
2
+ require 'showcase/helpers/seo_meta_builder'
3
+
4
+ module Showcase
5
+ module Traits
6
+ module Seo
7
+ extend ActiveSupport::Concern
8
+ include Base
9
+
10
+ module ClassMethods
11
+
12
+ def seo(name = nil, options = {}, &block)
13
+ define_method? [name, :seo_tags] do |options = {}|
14
+ meta = Helpers::ConfigObject.new(self, &block).to_hash
15
+ builder = Helpers::SeoMetaBuilder.new(view_context)
16
+ parts = %w(title description canonical_url image_url canonical_url).map(&:to_sym)
17
+ parts.map do |tag|
18
+ builder.send(tag, meta[tag], options) if meta[tag]
19
+ end.compact.join.html_safe
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+ end
27
+