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,54 @@
1
+ module Hanami
2
+ module View
3
+ # Inheriting mechanisms
4
+ #
5
+ # @since 0.1.0
6
+ module Inheritable
7
+ # Register a view subclass
8
+ #
9
+ # @api private
10
+ # @since 0.1.0
11
+ #
12
+ # @example
13
+ # require 'hanami/view'
14
+ #
15
+ # class IndexView
16
+ # include Hanami::View
17
+ # end
18
+ #
19
+ # class JsonIndexView < IndexView
20
+ # end
21
+ def inherited(base)
22
+ subclasses.add base
23
+ end
24
+
25
+ # Set of registered subclasses
26
+ #
27
+ # @api private
28
+ # @since 0.1.0
29
+ def subclasses
30
+ @subclasses ||= Set.new
31
+ end
32
+
33
+ protected
34
+ # Loading mechanism hook.
35
+ #
36
+ # @api private
37
+ # @since 0.1.0
38
+ #
39
+ # @see Hanami::View.load!
40
+ def load!
41
+ subclasses.freeze
42
+ views.freeze
43
+ end
44
+
45
+ # Registered views
46
+ #
47
+ # @api private
48
+ # @since 0.1.0
49
+ def views
50
+ @views ||= [ self ] + subclasses.to_a
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,265 @@
1
+ require 'hanami/view/rendering/registry'
2
+ require 'hanami/view/rendering/scope'
3
+
4
+ module Hanami
5
+ module View
6
+ # Rendering methods
7
+ #
8
+ # @since 0.1.0
9
+ #
10
+ # @see Hanami::View::Rendering::InstanceMethods
11
+ module Rendering
12
+ def self.extended(base)
13
+ base.class_eval do
14
+ include InstanceMethods
15
+ end
16
+ end
17
+
18
+ module InstanceMethods
19
+ # Initialize a view
20
+ #
21
+ # @param template [Hanami::View::Template] the template to render
22
+ # @param locals [Hash] a set of objects available during the rendering
23
+ # process.
24
+ #
25
+ # @since 0.1.0
26
+ #
27
+ # @see Hanami::View::Template
28
+ #
29
+ # @example
30
+ # require 'hanami/view'
31
+ #
32
+ # class IndexView
33
+ # include Hanami::View
34
+ # end
35
+ #
36
+ # template = Hanami::View::Template.new('index.html.erb')
37
+ # view = IndexView.new(template, {article: article})
38
+ def initialize(template, locals)
39
+ @template = template
40
+ @locals = locals
41
+ @scope = Scope.new(self, @locals)
42
+ end
43
+
44
+ # Render the template by bounding the local scope.
45
+ # If it uses a layout, it renders the template first and then the
46
+ # control passes to the layout.
47
+ #
48
+ # Override this method for custom rendering policies.
49
+ # For instance, when a serializer is used and there isn't the need of
50
+ # a template.
51
+ #
52
+ # @return [String] the output of the rendering process
53
+ #
54
+ # @raise [Hanami::View::MissingTemplateError] if the template is nil
55
+ #
56
+ # @since 0.1.0
57
+ #
58
+ # @see Hanami::View::Layout
59
+ #
60
+ # @example with template
61
+ # require 'hanami/view'
62
+ #
63
+ # class IndexView
64
+ # include Hanami::View
65
+ # end
66
+ #
67
+ # template = Hanami::View::Template.new('index.html.erb')
68
+ # view = IndexView.new(template, {article: article})
69
+ #
70
+ # view.render # => <h1>Introducing Hanami::view</h1> ...
71
+ #
72
+ # @example with template and layout
73
+ # require 'hanami/view'
74
+ #
75
+ # class ApplicationLayout
76
+ # include Hanami::View::Layout
77
+ # end
78
+ #
79
+ # class IndexView
80
+ # include Hanami::View
81
+ # layout :application
82
+ # end
83
+ #
84
+ # template = Hanami::View::Template.new('index.html.erb')
85
+ # view = IndexView.new(template, {article: article})
86
+ #
87
+ # view.render # => <html> ... <h1>Introducing Hanami::view</h1> ...
88
+ #
89
+ # @example with custom rendering
90
+ # require 'hanami/view'
91
+ #
92
+ # class IndexView
93
+ # include Hanami::View
94
+ #
95
+ # def render
96
+ # ArticleSerializer.new(article).render
97
+ # end
98
+ # end
99
+ #
100
+ # view = IndexView.new(nil, {article: article})
101
+ #
102
+ # view.render # => {title: ...}
103
+ def render
104
+ layout.render
105
+ end
106
+
107
+ protected
108
+ # The output of the template rendering process.
109
+ #
110
+ # @return [String] the rendering output
111
+ #
112
+ # @raise [Hanami::View::MissingTemplateError] if the template is nil
113
+ #
114
+ # @api private
115
+ # @since 0.1.0
116
+ def rendered
117
+ template.render @scope
118
+ end
119
+
120
+ # The layout.
121
+ #
122
+ # @return [Class, Hanami::View::Rendering::NullLayout]
123
+ #
124
+ # @see Hanami::View::Layout
125
+ # @see Hanami::View.layout
126
+ # @see Hanami::View::Dsl#layout
127
+ #
128
+ # @api private
129
+ # @since 0.1.0
130
+ def layout
131
+ @layout ||= self.class.layout.new(@scope, rendered)
132
+ end
133
+
134
+ # The template.
135
+ #
136
+ # @return [Hanami::View::Template] the template
137
+ #
138
+ # @raise [Hanami::View::MissingTemplateError] if the template is nil
139
+ #
140
+ # @api private
141
+ # @since 0.1.0
142
+ def template
143
+ @template or raise MissingTemplateError.new(self.class.template, @scope.format)
144
+ end
145
+
146
+ # A set of objects available during the rendering process.
147
+ #
148
+ # @return [Hash]
149
+ #
150
+ # @see Hanami::View#initialize
151
+ #
152
+ # @api private
153
+ # @since 0.1.0
154
+ def locals
155
+ @locals
156
+ end
157
+
158
+ # Delegates missing methods to the scope.
159
+ #
160
+ # @see Hanami::View::Rendering::Scope
161
+ #
162
+ # @api private
163
+ # @since 0.1.0
164
+ #
165
+ # @example
166
+ # require 'hanami/view'
167
+ #
168
+ # class IndexView
169
+ # include Hanami::View
170
+ # end
171
+ #
172
+ # template = Hanami::View::Template.new('index.html.erb')
173
+ # view = IndexView.new(template, {article: article})
174
+ #
175
+ # view.article # => #<Article:0x007fb0bbd3b6e8>
176
+ def method_missing(m)
177
+ @scope.__send__ m
178
+ end
179
+ end
180
+
181
+ # Render the given context and locals with the appropriate template.
182
+ # If there are registered subclasses, it choose the right class, according
183
+ # to the requested format.
184
+ #
185
+ # @param context [Hash] the context for the rendering process
186
+ # @option context [Symbol] :format the requested format
187
+ #
188
+ # @return [String] the output of the rendering process
189
+ #
190
+ # @raise [Hanami::View::MissingTemplateError] if it can't find a template
191
+ # for the given context
192
+ #
193
+ # @raise [Hanami::View::MissingFormatError] if the given context doesn't
194
+ # have the :format key
195
+ #
196
+ # @since 0.1.0
197
+ #
198
+ # @see Hanami::View#initialize
199
+ # @see Hanami::View#render
200
+ #
201
+ # @example
202
+ # require 'hanami/view'
203
+ #
204
+ # article = OpenStruct.new(title: 'Hello')
205
+ #
206
+ # module Articles
207
+ # class Show
208
+ # include Hanami::View
209
+ #
210
+ # def title
211
+ # @title ||= article.title.upcase
212
+ # end
213
+ # end
214
+ #
215
+ # class JsonShow < Show
216
+ # format :json
217
+ #
218
+ # def title
219
+ # super.downcase
220
+ # end
221
+ # end
222
+ # end
223
+ #
224
+ # Hanami::View.root = '/path/to/templates'
225
+ # Hanami::View.load!
226
+ #
227
+ # Articles::Show.render(format: :html, article: article)
228
+ # # => renders `articles/show.html.erb`
229
+ #
230
+ # Articles::Show.render(format: :json, article: article)
231
+ # # => renders `articles/show.json.erb`
232
+ #
233
+ # Articles::Show.render(format: :xml, article: article)
234
+ # # => raises Hanami::View::MissingTemplateError
235
+ def render(context)
236
+ registry.resolve(context).render
237
+ end
238
+
239
+ protected
240
+
241
+ # Loading mechanism hook.
242
+ #
243
+ # @api private
244
+ # @since 0.1.0
245
+ #
246
+ # @see Hanami::View.load!
247
+ def load!
248
+ super
249
+ registry.freeze
250
+ end
251
+
252
+ private
253
+
254
+ # The registry that holds all the registered subclasses.
255
+ #
256
+ # @api private
257
+ # @since 0.1.0
258
+ #
259
+ # @see Hanami::View::Rendering::Registry
260
+ def registry
261
+ @registry ||= Registry.new(self)
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,128 @@
1
+ require 'hanami/utils/string'
2
+ require 'hanami/utils/class'
3
+ require 'hanami/view/rendering/null_layout'
4
+
5
+ module Hanami
6
+ module View
7
+ module Rendering
8
+ # Defines the logic to find a layout
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ #
13
+ # @see Hanami::Layout
14
+ class LayoutFinder
15
+ # Layout class name suffix
16
+ #
17
+ # @api private
18
+ # @since 0.1.0
19
+ SUFFIX = 'Layout'.freeze
20
+
21
+ # Find a layout from the given name.
22
+ #
23
+ # @param layout [Symbol,String,NilClass] layout name or nil if you want
24
+ # to fallback to the framework defaults (see `Hanami::View.layout`).
25
+ #
26
+ # @param namespace [Class,Module] a Ruby namespace where to lookup
27
+ #
28
+ # @return [Hanami::Layout] the layout for the given name or
29
+ # `Hanami::View.layout`
30
+ #
31
+ # @api private
32
+ # @since 0.1.0
33
+ #
34
+ # @example With given name
35
+ # require 'hanami/view'
36
+ #
37
+ # Hanami::View::Rendering::LayoutFinder.find(:article) # =>
38
+ # ArticleLayout
39
+ #
40
+ # @example With a class
41
+ # require 'hanami/view'
42
+ #
43
+ # Hanami::View::Rendering::LayoutFinder.find(ArticleLayout) # =>
44
+ # ArticleLayout
45
+ #
46
+ # @example With namespace
47
+ # require 'hanami/view'
48
+ #
49
+ # Hanami::View::Rendering::LayoutFinder.find(:application, CardDeck) # =>
50
+ # CardDeck::ApplicationLayout
51
+ #
52
+ # @example With nil
53
+ # require 'hanami/view'
54
+ #
55
+ # Hanami::View::Rendering::LayoutFinder.find(nil) # =>
56
+ # Hanami::View::Rendering::NullLayout
57
+ #
58
+ # @example With unknown layout
59
+ # require 'hanami/view'
60
+ #
61
+ # Hanami::View::Rendering::LayoutFinder.find(:unknown) # =>
62
+ # Hanami::View::Rendering::NullLayout
63
+ #
64
+ def self.find(layout, namespace = Object)
65
+ case layout
66
+ when Symbol, String
67
+ # TODO Move this low level logic into a Hanami::Utils solution
68
+ class_name = "#{ Utils::String.new(layout).classify }#{ SUFFIX }"
69
+ namespace = Utils::Class.load_from_pattern!(namespace)
70
+ namespace.const_get(class_name)
71
+ when Class
72
+ layout
73
+ end || NullLayout
74
+ end
75
+
76
+ # Initialize the finder
77
+ #
78
+ # @param view [Class, #layout]
79
+ #
80
+ # @api private
81
+ # @since 0.1.0
82
+ def initialize(view)
83
+ @view = view
84
+ end
85
+
86
+ # Find the layout for the view
87
+ #
88
+ # @return [Hanami::Layout] the layout associated to the view
89
+ #
90
+ # @see Hanami::View::Rendering::LayoutFinder.find
91
+ # @see Hanami::View::Rendering::LayoutFinder#initialize
92
+ #
93
+ # @api private
94
+ # @since 0.1.0
95
+ #
96
+ # @example With layout
97
+ # require 'hanami/view'
98
+ #
99
+ # module Articles
100
+ # class Show
101
+ # include Hanami::View
102
+ # layout :article
103
+ # end
104
+ # end
105
+ #
106
+ # Hanami::View::Rendering::LayoutFinder.new(Articles::Show) # =>
107
+ # ArticleLayout
108
+ #
109
+ # @example Without layout
110
+ # require 'hanami/view'
111
+ #
112
+ # module Dashboard
113
+ # class Index
114
+ # include Hanami::View
115
+ # end
116
+ # end
117
+ #
118
+ # Hanami::View.layout # => :application
119
+ #
120
+ # Hanami::View::Rendering::LayoutFinder.new(Dashboard::Index) # =>
121
+ # ApplicationLayout
122
+ def find
123
+ self.class.find(@view.layout)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,63 @@
1
+ require 'hanami/view/rendering/null_template'
2
+ require 'hanami/view/rendering/templates_finder'
3
+
4
+ module Hanami
5
+ module View
6
+ module Rendering
7
+ # Holds the references of all the registered layouts.
8
+ # As now the registry is unique at the level of the framework.
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ #
13
+ # @see Hanami::Layout::ClassMethods#registry
14
+ class LayoutRegistry
15
+ # Initialize the registry
16
+ #
17
+ # @param view [Class] the view
18
+ #
19
+ # @api private
20
+ # @since 0.1.0
21
+ def initialize(view)
22
+ @registry = {}
23
+ @view = view
24
+ prepare!
25
+ end
26
+
27
+ # Returns the layout for the given context.
28
+ #
29
+ # @param context [Hash] the rendering context
30
+ # @option context [Symbol] :format the requested format
31
+ #
32
+ # @return [Hanami::Layout, Hanami::View::Rendering::NullTemplate]
33
+ # the layout associated with the given context or a `NullTemplate` if
34
+ # it can't be found.
35
+ #
36
+ # @raise [Hanami::View::MissingFormatError] if the given context doesn't
37
+ # have the :format key
38
+ #
39
+ # @api private
40
+ # @since 0.1.0
41
+ def resolve(context)
42
+ @registry.fetch(format(context)) { NullTemplate.new }
43
+ end
44
+
45
+ protected
46
+ def prepare!
47
+ templates.each do |template|
48
+ @registry.merge! template.format => template
49
+ end
50
+ @registry.any? or raise MissingTemplateLayoutError.new(@view)
51
+ end
52
+
53
+ def templates
54
+ TemplatesFinder.new(@view).find
55
+ end
56
+
57
+ def format(context)
58
+ context.fetch(:format) { raise MissingFormatError }
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end