hanami-view 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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