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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -0
- data/LICENSE.md +22 -0
- data/README.md +826 -7
- data/hanami-view.gemspec +17 -12
- data/lib/hanami-view.rb +1 -0
- data/lib/hanami/layout.rb +142 -0
- data/lib/hanami/presenter.rb +126 -0
- data/lib/hanami/view.rb +259 -2
- data/lib/hanami/view/configuration.rb +464 -0
- data/lib/hanami/view/dsl.rb +346 -0
- data/lib/hanami/view/errors.rb +47 -0
- data/lib/hanami/view/escape.rb +180 -0
- data/lib/hanami/view/inheritable.rb +54 -0
- data/lib/hanami/view/rendering.rb +265 -0
- data/lib/hanami/view/rendering/layout_finder.rb +128 -0
- data/lib/hanami/view/rendering/layout_registry.rb +63 -0
- data/lib/hanami/view/rendering/layout_scope.rb +240 -0
- data/lib/hanami/view/rendering/null_layout.rb +52 -0
- data/lib/hanami/view/rendering/null_template.rb +83 -0
- data/lib/hanami/view/rendering/partial.rb +29 -0
- data/lib/hanami/view/rendering/partial_finder.rb +73 -0
- data/lib/hanami/view/rendering/registry.rb +128 -0
- data/lib/hanami/view/rendering/scope.rb +88 -0
- data/lib/hanami/view/rendering/template.rb +67 -0
- data/lib/hanami/view/rendering/template_finder.rb +53 -0
- data/lib/hanami/view/rendering/template_name.rb +37 -0
- data/lib/hanami/view/rendering/templates_finder.rb +129 -0
- data/lib/hanami/view/rendering/view_finder.rb +37 -0
- data/lib/hanami/view/template.rb +43 -0
- data/lib/hanami/view/version.rb +4 -1
- metadata +91 -16
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -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
|