hanami-view 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ require 'hanami/utils/escape'
2
+
3
+ module Hanami
4
+ module View
5
+ module Rendering
6
+ # Scope for layout rendering
7
+ #
8
+ # @since 0.1.0
9
+ class LayoutScope < BasicObject
10
+ # Initialize the scope
11
+ #
12
+ # @param layout [Hanami::Layout] the layout to render
13
+ # @param scope [Hanami::View::Rendering::Scope] the scope of the current
14
+ # view
15
+ #
16
+ # @api private
17
+ # @since 0.1.0
18
+ def initialize(layout, scope)
19
+ @layout, @scope = layout, scope
20
+ end
21
+
22
+ # Returns the classname as string
23
+ #
24
+ # @return classname
25
+ #
26
+ # @since 0.3.0
27
+ def class
28
+ (class << self; self end).superclass
29
+ end
30
+
31
+ # Returns an inspect String
32
+ #
33
+ # @return [String] inspect String (contains classname, objectid in hex, available ivars)
34
+ #
35
+ # @since 0.3.0
36
+ def inspect
37
+ base = "#<#{ self.class }:#{'%x' % (self.object_id << 1)}"
38
+ base << " @layout=\"#{@layout.inspect}\"" if @layout
39
+ base << " @scope=\"#{@scope.inspect}\"" if @scope
40
+ base << ">"
41
+ end
42
+
43
+ # Render a partial or a template within a layout template.
44
+ #
45
+ # @param options [Hash]
46
+ # @option options [String] :partial the partial template to render
47
+ # @option options [String] :template the template to render
48
+ #
49
+ # @return [String] the output of the rendering process
50
+ #
51
+ # @since 0.1.0
52
+ #
53
+ # @example Rendering partial
54
+ # # Given a partial under:
55
+ # # templates/shared/_sidebar.html.erb
56
+ # #
57
+ # # In the layout template:
58
+ # # templates/application.html.erb
59
+ # #
60
+ # # Use like this:
61
+ # <%= render partial: 'shared/sidebar' %>
62
+ #
63
+ # @example Rendering template
64
+ # # Given a template under:
65
+ # # templates/articles/index.html.erb
66
+ # #
67
+ # # In the layout template:
68
+ # # templates/application.html.erb
69
+ # #
70
+ # # Use like this:
71
+ # <%= render template: 'articles/index' %>
72
+ #
73
+ # @example Rendering partial, using optional :locals
74
+ # # Given a partial under:
75
+ # # templates/shared/_sidebar.html.erb
76
+ # #
77
+ # # In the layout template:
78
+ # # templates/application.html.erb
79
+ # #
80
+ # # Use like this:
81
+ # <%= render partial: 'shared/sidebar', { user: current_user } %>
82
+ #
83
+ # #
84
+ # # `user` will be available in the scope of the sidebar rendering
85
+ def render(options)
86
+ renderer(options).render
87
+ end
88
+
89
+ # Returns the requested format.
90
+ #
91
+ # @return [Symbol] the requested format (eg. :html, :json, :xml, etc..)
92
+ #
93
+ # @since 0.1.0
94
+ def format
95
+ @scope.format
96
+ end
97
+
98
+ # The current view.
99
+ #
100
+ # @return [Hanami::View] the current view
101
+ #
102
+ # @since 0.1.0
103
+ def view
104
+ @view || @scope.view
105
+ end
106
+
107
+ # The current locals.
108
+ #
109
+ # @return [Hash] the current locals
110
+ #
111
+ # @since 0.1.0
112
+ def locals
113
+ @locals || @scope.locals
114
+ end
115
+
116
+ # Returns a content for the given key, by trying to invoke on the current
117
+ # scope, a method with the same name.
118
+ #
119
+ # The scope is made of locals and concrete methods from view and layout.
120
+ #
121
+ # @param key [Symbol] a method to invoke within current scope
122
+ # @return [String,NilClass] returning content if scope respond to the
123
+ # requested method
124
+ #
125
+ # @since 0.4.1
126
+ #
127
+ # @example
128
+ # # Given the following layout template
129
+ #
130
+ # <!doctype HTML>
131
+ # <html>
132
+ # <!-- ... -->
133
+ # <body>
134
+ # <!-- ... -->
135
+ # <%= content :footer %>
136
+ # </body>
137
+ # </html>
138
+ #
139
+ # # Case 1:
140
+ # # Products::Index doesn't respond to #footer, content will return nil
141
+ # #
142
+ # # Case 2:
143
+ # # Products::Show responds to #footer, content will send back
144
+ # # #footer returning value
145
+ #
146
+ # module Products
147
+ # class Index
148
+ # include Hanami::View
149
+ # end
150
+ #
151
+ # class Show
152
+ # include Hanami::View
153
+ #
154
+ # def footer
155
+ # "contents for footer"
156
+ # end
157
+ # end
158
+ # end
159
+ def content(key)
160
+ __send__(key) if respond_to?(key)
161
+ end
162
+
163
+ # Implements "respond to" logic
164
+ #
165
+ # @return [TrueClass,FalseClass]
166
+ #
167
+ # @since 0.3.0
168
+ #
169
+ # @see http://ruby-doc.org/core/Object.html#method-i-respond_to-3F
170
+ def respond_to?(m, include_all = false)
171
+ respond_to_missing?(m, include_all)
172
+ end
173
+
174
+ # Implements "respond to" logic
175
+ #
176
+ # @return [TrueClass,FalseClass]
177
+ #
178
+ # @since 0.3.0
179
+ # @api private
180
+ #
181
+ # @see http://ruby-doc.org/core/Object.html#method-i-respond_to_missing-3F
182
+ def respond_to_missing?(m, include_all)
183
+ @layout.respond_to?(m, include_all) ||
184
+ @scope.respond_to?(m, include_all)
185
+ end
186
+
187
+ protected
188
+ # Forward all the missing methods to the view scope or to the layout.
189
+ #
190
+ # @api private
191
+ # @since 0.1.0
192
+ #
193
+ # @see Hanami::View::Rendering::Scope
194
+ # @see Hanami::Layout
195
+ #
196
+ # @example
197
+ # # In the layout template:
198
+ # # templates/application.html.erb
199
+ # #
200
+ # # Use like this:
201
+ # <title><%= article.title %></title>
202
+ #
203
+ # # `article` will be looked up in the view scope first.
204
+ # # If not found, it will be searched within the layout.
205
+ def method_missing(m, *args, &blk)
206
+ if @scope.respond_to?(m)
207
+ @scope.__send__(m, *args, &blk)
208
+ elsif layout.respond_to?(m)
209
+ layout.__send__(m, *args, &blk)
210
+ else
211
+ ::Hanami::View::Escape.html(super)
212
+ end
213
+ end
214
+
215
+ def renderer(options)
216
+ if options[:partial]
217
+ Rendering::Partial
218
+ elsif options[:template]
219
+ Rendering::Template
220
+ end.new(view, _options(options))
221
+ end
222
+
223
+ private
224
+ def _options(options)
225
+ options.dup.tap do |opts|
226
+ opts.merge!(format: format)
227
+ opts[:locals] = locals
228
+ opts[:locals].merge!(options.fetch(:locals){ ::Hash.new })
229
+ end
230
+ end
231
+
232
+ # @since 0.4.2
233
+ # @api private
234
+ def layout
235
+ @layout || @layout.class.layout.new(@scope, "")
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,52 @@
1
+ module Hanami
2
+ module View
3
+ module Rendering
4
+ # Null Object pattern for Layout.
5
+ # It's used when a view doesn't require a layout.
6
+ #
7
+ # @api private
8
+ # @since 0.1.0
9
+ #
10
+ # @example
11
+ # require 'hanami/view'
12
+ #
13
+ # module Articles
14
+ # class Show
15
+ # include Hanami::View
16
+ # layout false
17
+ # end
18
+ # end
19
+ #
20
+ # # In this scenario we will use a `NullLayout`.
21
+ class NullLayout
22
+
23
+ # Initialize a layout
24
+ #
25
+ # @param scope [Hanami::View::Rendering::Scope] view rendering scope
26
+ # @param rendered [String] the output of the view rendering process
27
+ #
28
+ # @api private
29
+ # @since 0.1.0
30
+ #
31
+ # @see Hanami::Layout#initialize
32
+ # @see Hanami::View::Rendering#render
33
+ def initialize(scope, rendered)
34
+ @rendered = rendered
35
+ end
36
+
37
+ # Render the layout
38
+ #
39
+ # @return [String] the output of the rendering process
40
+ #
41
+ # @api private
42
+ # @since 0.1.0
43
+ #
44
+ # @see Hanami::Layout#render
45
+ # @see Hanami::View::Rendering#render
46
+ def render
47
+ @rendered
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,83 @@
1
+ module Hanami
2
+ module View
3
+ module Rendering
4
+ # Null Object pattern for layout template
5
+ #
6
+ # It's used when a layout doesn't have an associated template.
7
+ #
8
+ # A common scenario is for non-html requests.
9
+ # Usually we have a template for the application layout
10
+ # (eg `templates/application.html.erb`), but we don't use to have the a
11
+ # template for JSON requests (eg `templates/application.json.erb`).
12
+ # Because most of the times, we only return the output of the view.
13
+ #
14
+ # @api private
15
+ # @since 0.1.0
16
+ #
17
+ # @example
18
+ # require 'hanami/view'
19
+ #
20
+ # # We have an ApplicationLayout (views/application_layout.rb):
21
+ # class ApplicationLayout
22
+ # include Hanami::Layout
23
+ # end
24
+ #
25
+ # # Our layout has a template for HTML requests, located at:
26
+ # # templates/application.html.erb
27
+ #
28
+ # # We set it as global layout
29
+ # Hanami::View.layout = :application
30
+ #
31
+ # # We have two views for HTML and JSON articles.
32
+ # # They have a template each:
33
+ # #
34
+ # # * templates/articles/show.html.erb
35
+ # # * templates/articles/show.json.erb
36
+ # module Articles
37
+ # class Show
38
+ # include Hanami::View
39
+ # format :html
40
+ # end
41
+ #
42
+ # class JsonShow < Show
43
+ # format :json
44
+ # end
45
+ # end
46
+ #
47
+ # # We initialize the framework
48
+ # Hanami::View.load!
49
+ #
50
+ #
51
+ #
52
+ # # When we ask for a HTML rendering, it will use `Articles::Show` and
53
+ # # ApplicationLayout. The output will be a composition of:
54
+ # #
55
+ # # * templates/articles/show.html.erb
56
+ # # * templates/application.html.erb
57
+ #
58
+ # # When we ask for a JSON rendering, it will use `Articles::JsonShow`
59
+ # # and ApplicationLayout. Since, the layout doesn't have any associated
60
+ # # template for JSON, the output will be a composition of:
61
+ # #
62
+ # # * templates/articles/show.json.erb
63
+ class NullTemplate
64
+ # Render the layout template
65
+ #
66
+ # @param scope [Hanami::View::Scope] the rendering scope
67
+ # @param locals [Hash] a set of objects available during the rendering
68
+ # @yield [Proc] yields the given block
69
+ #
70
+ # @return [String] the output of the rendering process
71
+ #
72
+ # @api private
73
+ # @since 0.1.0
74
+ #
75
+ # @see Hanami::Layout#render
76
+ # @see Hanami::View::Rendering#render
77
+ def render(scope, locals = {})
78
+ yield
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,29 @@
1
+ require 'hanami/view/rendering/partial_finder'
2
+
3
+ module Hanami
4
+ module View
5
+ module Rendering
6
+ # Rendering partial
7
+ #
8
+ # It's used when a template wants to render a partial.
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ #
13
+ # @see Hanami::View::Rendering::Template
14
+ # @see Hanami::View::Rendering::LayoutScope#render
15
+ #
16
+ # @example
17
+ # # We have an application template (templates/application.html.erb)
18
+ # # that uses the following line:
19
+ #
20
+ # <%= render partial: 'shared/sidebar' %>
21
+ class Partial < Template
22
+ protected
23
+ def template
24
+ PartialFinder.new(@view.class, @options).find
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,73 @@
1
+ require 'hanami/view/rendering/template_finder'
2
+
3
+ module Hanami
4
+ module View
5
+ module Rendering
6
+ # Find a partial for the current view context.
7
+ # It's used when a template wants to render a partial.
8
+ #
9
+ # @see Hanami::View::Rendering::Partial
10
+ # @see Hanami::View::Rendering::TemplateFinder
11
+ #
12
+ # @api private
13
+ # @since 0.1.0
14
+ class PartialFinder < TemplateFinder
15
+ # Template file name prefix.
16
+ # By convention a partial file name starts with this prefix.
17
+ #
18
+ # @api private
19
+ # @since 0.1.0
20
+ #
21
+ # @example
22
+ # "_sidebar.html.erb"
23
+ PREFIX = '_'.freeze
24
+
25
+ # Find a template for a partial. Initially it will look for the
26
+ # partial template under the directory of the parent directory
27
+ # view template, if not found it will search recursivly from
28
+ # the view root.
29
+ #
30
+ # @return [Hanami::View::Template] the requested template
31
+ #
32
+ # @see Hanami::View::Rendering::TemplateFinder#find
33
+ #
34
+ # @since 0.4.3
35
+ # @api private
36
+ def find
37
+ if path = partial_template_under_view_path
38
+ View::Template.new(path, @view.configuration.default_encoding)
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ protected
45
+ # @since 0.4.3
46
+ # @api private
47
+ def partial_template_under_view_path
48
+ _find(view_template_dir).first
49
+ end
50
+
51
+ # @since 0.4.3
52
+ # @api private
53
+ def view_template_dir
54
+ *all, _ = @view.template.split(separator)
55
+ all.join(separator)
56
+ end
57
+
58
+ def template_name
59
+ *all, last = partial_name.split(separator)
60
+ all.push( last.prepend(prefix) ).join(separator)
61
+ end
62
+
63
+ def partial_name
64
+ @options[:partial]
65
+ end
66
+
67
+ def prefix
68
+ PREFIX
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end