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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0fa2f0c09d6500c146316de763a71a98afefdf03
|
4
|
+
data.tar.gz: 7d96ea6c912cdd30fc2945c786c6a5d057cffc98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f818e7eb0e57bf05852b7f8a1b6fe1971ca87d9d20404815d179aa7202eb07c3627bc996e29136babff7d1b2072f531748b6401361e7d7e5a179ed2b7f25979a
|
7
|
+
data.tar.gz: 675e51826977c6e4bf1872aa4350e95d68deb225833a8a821b66ac686062f5b3418f9a19918572c850f5d9746bbc10a9e401382da4c27df9fab5d587bb1a9592
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Hanami::View
|
2
|
+
View layer for Hanami
|
3
|
+
|
4
|
+
## v0.6.0 - 2016-01-22
|
5
|
+
### Changed
|
6
|
+
- [Luca Guidi] Renamed the project
|
7
|
+
|
8
|
+
## v0.5.0 - 2016-01-12
|
9
|
+
### Added
|
10
|
+
- [Luca Guidi] Added `Lotus::View::Configuration#default_encoding` to set the encoding for templates
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
- [Luca Guidi] Let exceptions to be raised as they occur in rendering context. This fixes misleading backtraces for exceptions.
|
14
|
+
- [Martin Rubi] Raise a `Lotus::View::MissingTemplateError` when rendering a missing partial from a template
|
15
|
+
- [Luca Guidi] Fix for `template.erb is not valid US-ASCII (Encoding::InvalidByteSequenceError)` when system encoding is not set
|
16
|
+
|
17
|
+
### Changed
|
18
|
+
- [Liam Dawson] Introduced `Lotus::View::Error` and let all the framework exceptions to inherit from it.
|
19
|
+
|
20
|
+
## v0.4.4 - 2015-09-30
|
21
|
+
### Added
|
22
|
+
- [Luca Guidi] Autoescape for layout helpers.
|
23
|
+
|
24
|
+
## v0.4.3 - 2015-07-10
|
25
|
+
### Fixed
|
26
|
+
- [Farrel Lifson] Force partial finder to be explicit when to templates have the same name.
|
27
|
+
|
28
|
+
## v0.4.2 - 2015-06-23
|
29
|
+
### Fixed
|
30
|
+
- [Tom Kadwill] Ensure views to use methods defined by the associated layout.
|
31
|
+
|
32
|
+
## v0.4.1 - 2015-05-22
|
33
|
+
### Added
|
34
|
+
- [Luca Guidi] Introduced `#content` to render optional contents in a different context (eg. a view sets a page specific javascript in the application template footer).
|
35
|
+
|
36
|
+
## v0.4.0 - 2015-03-23
|
37
|
+
### Changed
|
38
|
+
- [Luca Guidi] Autoescape concrete and virtual methods from presenters
|
39
|
+
- [Luca Guidi] Autoescape concrete and virtual methods from views
|
40
|
+
|
41
|
+
### Fixed
|
42
|
+
- [Tom Kadwill] Improve error message for undefined method in view
|
43
|
+
- [Luca Guidi] Ensure that layouts will include modules from `Configuration#prepare`
|
44
|
+
|
45
|
+
## v0.3.0 - 2014-12-23
|
46
|
+
### Added
|
47
|
+
- [Trung Lê] When duplicate the framework, also duplicate `Presenter`
|
48
|
+
- [Benny Klotz] Introduced `Scope#class`, `#inspect`, `LayoutScope#class` and `#inspect`
|
49
|
+
- [Alfonso Uceda Pompa & Trung Lê] Introduced `Configuration#prepare`
|
50
|
+
- [Luca Guidi] Implemented "respond to" logic for `Lotus::View::Scope` (`respond_to?` and `respond_to_missing?`)
|
51
|
+
- [Luca Guidi] Implemented "respond to" logic for `Lotus::Layout` (`respond_to?` and `respond_to_missing?`)
|
52
|
+
- [Jeremy Stephens] Allow view concrete methods that accept a block to be invoked from templates
|
53
|
+
- [Peter Suschlik] Implemented "respond to" logic for `Lotus::Presenter` (`respond_to?` and `respond_to_missing?`)
|
54
|
+
- [Luca Guidi] Official support for Ruby 2.2
|
55
|
+
|
56
|
+
### Changed
|
57
|
+
- [Alfonso Uceda Pompa] Raise an exception when a layout doesn't have an associated template
|
58
|
+
|
59
|
+
### Fixed
|
60
|
+
- [Luca Guidi] Ensure that concrete methods in layouts are available in templates
|
61
|
+
- [Luca Guidi] Ensure to associate the right layout to a view in case fo duplicated framework
|
62
|
+
- [Luca Guidi] Safe override of Ruby's top level methods in Scope. (Eg. use `select` from a view, not from `::Kernel`)
|
63
|
+
|
64
|
+
## v0.2.0 - 2014-06-23
|
65
|
+
### Added
|
66
|
+
- [Luca Guidi] Introduced `Configuration#duplicate`
|
67
|
+
- [Luca Guidi] Introduced `Configuration#layout` to define the layout that all the views will use
|
68
|
+
- [Luca Guidi] Introduced `Configuration#load_paths` to define several sources where to lookup for templates
|
69
|
+
- [Luca Guidi] Introduced `Configuration#root` to define the root path where to find templates
|
70
|
+
- [Luca Guidi] Introduced `Lotus::View::Configuration`
|
71
|
+
- [Grant Ammons] Allow view concrete methods with arity > 0 to be invoked from templates
|
72
|
+
- [Luca Guidi] Official support for Ruby 2.1
|
73
|
+
|
74
|
+
### Changed
|
75
|
+
- [Luca Guidi] `Rendering::TemplatesFinder` now look recursively for templates, starting from the root.
|
76
|
+
- [Luca Guidi] Removed `View.layout=`
|
77
|
+
- [Luca Guidi] Removed `View.root=`
|
78
|
+
|
79
|
+
### Fixed
|
80
|
+
- [Luca Guidi] Ensure outermost locals to not shadow innermost inside templates/partials
|
81
|
+
|
82
|
+
## v0.1.0 - 2014-03-23
|
83
|
+
### Added
|
84
|
+
- [Luca Guidi] Allow custom rendering policy via `Action#render` override. This bypasses the template lookup and rendering.
|
85
|
+
- [Luca Guidi] Introduced `Lotus::Presenter`
|
86
|
+
- [Luca Guidi] Introduced templates rendering from templates and layouts
|
87
|
+
- [Luca Guidi] Introduced partials rendering from templates and layouts
|
88
|
+
- [Luca Guidi] Introduced layouts support
|
89
|
+
- [Luca Guidi] Introduced `Lotus::View.load!` as entry point to load views and templates
|
90
|
+
- [Luca Guidi] Allow to setup template name via `View.template`
|
91
|
+
- [Luca Guidi] Rendering context also considers locals passed to the constructor
|
92
|
+
- [Luca Guidi] Introduced `View.format` as DSL to declare which format to handle
|
93
|
+
- [Luca Guidi] Introduced view subclasses as way to handle different formats (mime types)
|
94
|
+
- [Luca Guidi] Introduced multiple templates per each View
|
95
|
+
- [Luca Guidi] Implemented basic rendering with templates
|
96
|
+
- [Luca Guidi] Official support for Ruby 2.0
|
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014-2016 Luca Guidi
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,8 +1,40 @@
|
|
1
1
|
# Hanami::View
|
2
2
|
|
3
|
-
|
3
|
+
A View layer for [Hanami](http://hanamirb.org).
|
4
4
|
|
5
|
-
|
5
|
+
It's based on a **separation between views and templates**.
|
6
|
+
|
7
|
+
A _view_ is an object that encapsulates the presentation logic of a page.
|
8
|
+
A _template_ is a file that defines the semantic and visual elements of a page.
|
9
|
+
In order to show a result to a user, a template must be _rendered_ by a view.
|
10
|
+
|
11
|
+
Keeping things separated helps to declutter templates and models from presentation logic.
|
12
|
+
Also, since views are objects, they are easily testable.
|
13
|
+
If you ever used [Mustache](http://mustache.github.io/), you are already aware of the advantages.
|
14
|
+
|
15
|
+
Like all the other Hanami components, it can be used as a standalone framework or within a full Hanami application.
|
16
|
+
|
17
|
+
## Status
|
18
|
+
|
19
|
+
[](https://badge.fury.io/rb/hanami-view)
|
20
|
+
[](https://travis-ci.org/hanami/view?branch=master)
|
21
|
+
[](https://coveralls.io/r/hanami/view)
|
22
|
+
[](https://codeclimate.com/github/hanami/view)
|
23
|
+
[](https://gemnasium.com/hanami/view)
|
24
|
+
[](http://inch-ci.org/github/hanami/view)
|
25
|
+
|
26
|
+
## Contact
|
27
|
+
|
28
|
+
* Home page: http://hanamirb.org
|
29
|
+
* Mailing List: http://hanamirb.org/mailing-list
|
30
|
+
* API Doc: http://rdoc.info/gems/hanami-view
|
31
|
+
* Bugs/Issues: https://github.com/hanami/view/issues
|
32
|
+
* Support: http://stackoverflow.com/questions/tagged/hanami
|
33
|
+
* Chat: http://chat.hanamirb.org
|
34
|
+
|
35
|
+
## Rubies
|
36
|
+
|
37
|
+
__Hanami::View__ supports Ruby (MRI) 2+
|
6
38
|
|
7
39
|
## Installation
|
8
40
|
|
@@ -22,15 +54,802 @@ Or install it yourself as:
|
|
22
54
|
|
23
55
|
## Usage
|
24
56
|
|
25
|
-
|
57
|
+
### Conventions
|
58
|
+
|
59
|
+
* Templates are searched under `Hanami::View.configuration.root`, set this value according to your app structure (eg. `"app/templates"`).
|
60
|
+
* A view will look for a template with a file name that is composed by its full class name (eg. `"articles/index"`).
|
61
|
+
* A template must have two concatenated extensions: one for the format and one for the engine (eg. `".html.erb"`).
|
62
|
+
* The framework must be loaded before rendering the first time: `Hanami::View.load!`.
|
63
|
+
|
64
|
+
### Views
|
65
|
+
|
66
|
+
A simple view looks like this:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
require 'hanami/view'
|
70
|
+
|
71
|
+
module Articles
|
72
|
+
class Index
|
73
|
+
include Hanami::View
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
Suppose that we want to render a list of `articles`:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
require 'hanami/view'
|
82
|
+
|
83
|
+
module Articles
|
84
|
+
class Index
|
85
|
+
include Hanami::View
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
Hanami::View.configure do
|
90
|
+
root 'app/templates'
|
91
|
+
end
|
92
|
+
|
93
|
+
Hanami::View.load!
|
94
|
+
|
95
|
+
path = Hanami::View.configuration.root.join('articles/index.html.erb')
|
96
|
+
template = Hanami::View::Template.new(path)
|
97
|
+
articles = ArticleRepository.all
|
98
|
+
|
99
|
+
Articles::Index.new(template, articles: articles).render
|
100
|
+
```
|
101
|
+
|
102
|
+
While this code is working fine, it's inefficient and verbose, because we are loading a template from the filesystem for each rendering attempt.
|
103
|
+
Also, this is strictly related to the HTML format, what if we want to manage other formats?
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
require 'hanami/view'
|
107
|
+
|
108
|
+
module Articles
|
109
|
+
class Index
|
110
|
+
include Hanami::View
|
111
|
+
end
|
112
|
+
|
113
|
+
class AtomIndex < Index
|
114
|
+
format :atom
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
Hanami::View.configure do
|
119
|
+
root 'app/templates'
|
120
|
+
end
|
121
|
+
|
122
|
+
Hanami::View.load!
|
123
|
+
|
124
|
+
articles = ArticleRepository.all
|
125
|
+
|
126
|
+
Articles::Index.render(format: :html, articles: articles)
|
127
|
+
# => This will use Articles::Index
|
128
|
+
# and "articles/index.html.erb"
|
129
|
+
|
130
|
+
Articles::Index.render(format: :atom, articles: articles)
|
131
|
+
# => This will use Articles::AtomIndex
|
132
|
+
# and "articles/index.atom.erb"
|
133
|
+
|
134
|
+
Articles::Index.render(format: :xml, articles: articles)
|
135
|
+
# => This will raise a Hanami::View::MissingTemplateError
|
136
|
+
```
|
137
|
+
|
138
|
+
### Locals
|
139
|
+
|
140
|
+
All the objects passed in the context are called _locals_, they are available both in the view and in the template:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
require 'hanami/view'
|
144
|
+
|
145
|
+
module Articles
|
146
|
+
class Show
|
147
|
+
include Hanami::View
|
148
|
+
|
149
|
+
def authors
|
150
|
+
article.authors.map(&:full_name).join ', '
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
```erb
|
157
|
+
<h1><%= article.title %></h1>
|
158
|
+
<article>
|
159
|
+
<%= article.content %>
|
160
|
+
</article>
|
161
|
+
```
|
162
|
+
|
163
|
+
All the methods defined in the view are accessible from the template:
|
164
|
+
|
165
|
+
```erb
|
166
|
+
<h2><%= authors %></h2>
|
167
|
+
```
|
168
|
+
|
169
|
+
For convenience, they are also available to the view as a Hash, accessed through the `locals` method.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
require 'hanami/view'
|
173
|
+
|
174
|
+
module Articles
|
175
|
+
class Show
|
176
|
+
include Hanami::View
|
177
|
+
|
178
|
+
# This view already responds to `#article` because there is an element in
|
179
|
+
# the locals with the same key.
|
180
|
+
#
|
181
|
+
# In order to allow developers to override those methods, and decorate a
|
182
|
+
# single locals object, a view has a Hash with the same values.
|
183
|
+
#
|
184
|
+
# If we had implemented this method like this:
|
185
|
+
#
|
186
|
+
# def article
|
187
|
+
# ArticlePresenter.new(article)
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# We would have generated a `SystemStackError` (stack level too deep).
|
191
|
+
def article
|
192
|
+
ArticlePresenter.new(locals[:article])
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
### Custom rendering
|
199
|
+
|
200
|
+
Since a view is an object, you can override `#render` and provide your own rendering policy:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
require 'hanami/view'
|
204
|
+
|
205
|
+
module Articles
|
206
|
+
class Show
|
207
|
+
include Hanami::View
|
208
|
+
format :json
|
209
|
+
|
210
|
+
def render
|
211
|
+
ArticleSerializer.new(article).to_json
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
Articles::Show.render({format: :json, article: article})
|
217
|
+
# => This will render from ArticleSerializer,
|
218
|
+
# without the need of a template
|
219
|
+
```
|
220
|
+
|
221
|
+
### Format
|
222
|
+
|
223
|
+
The `.format` DSL is used to declare one or more mime types that a view is able to render.
|
224
|
+
These values are **arbitrary**, just **be sure to create a corresponding template**.
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
require 'hanami/view'
|
228
|
+
|
229
|
+
module Articles
|
230
|
+
class Show
|
231
|
+
include Hanami::View
|
232
|
+
format :custom
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
Articles::Show.render({format: :custom, article: article})
|
237
|
+
# => This will render "articles/show.custom.erb"
|
238
|
+
```
|
239
|
+
|
240
|
+
### Engines
|
241
|
+
|
242
|
+
The builtin rendering engine is [ERb](http://en.wikipedia.org/wiki/ERuby).
|
243
|
+
However, Hanami::View supports countless rendering engines out of the box.
|
244
|
+
Require your library of choice **before** requiring `'hanami/view'`, and it will just work.
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
require 'haml'
|
248
|
+
require 'hanami/view'
|
249
|
+
|
250
|
+
module Articles
|
251
|
+
class Show
|
252
|
+
include Hanami::View
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
Articles::Show.render({format: :html, article: article})
|
257
|
+
# => This will render "articles/show.html.haml"
|
258
|
+
```
|
259
|
+
|
260
|
+
This is the list of the supported engines.
|
261
|
+
They are listed in order of **higher precedence**, for a given extension.
|
262
|
+
For instance, if [ERubis](http://www.kuwata-lab.com/erubis/) is loaded, it will be preferred over ERb to render `.erb` templates.
|
263
|
+
|
264
|
+
<table>
|
265
|
+
<tr>
|
266
|
+
<th>Engine</th>
|
267
|
+
<th>Extensions</th>
|
268
|
+
</tr>
|
269
|
+
<tr>
|
270
|
+
<td>Erubis</td>
|
271
|
+
<td>erb, rhtml, erubis</td>
|
272
|
+
</tr>
|
273
|
+
<tr>
|
274
|
+
<td>ERb</td>
|
275
|
+
<td>erb, rhtml</td>
|
276
|
+
</tr>
|
277
|
+
<tr>
|
278
|
+
<td>Redcarpet</td>
|
279
|
+
<td>markdown, mkd, md</td>
|
280
|
+
</tr>
|
281
|
+
<tr>
|
282
|
+
<td>RDiscount</td>
|
283
|
+
<td>markdown, mkd, md</td>
|
284
|
+
</tr>
|
285
|
+
<tr>
|
286
|
+
<td>Kramdown</td>
|
287
|
+
<td>markdown, mkd, md</td>
|
288
|
+
</tr>
|
289
|
+
<tr>
|
290
|
+
<td>Maruku</td>
|
291
|
+
<td>markdown, mkd, md</td>
|
292
|
+
</tr>
|
293
|
+
<tr>
|
294
|
+
<td>BlueCloth</td>
|
295
|
+
<td>markdown, mkd, md</td>
|
296
|
+
</tr>
|
297
|
+
<tr>
|
298
|
+
<td>Asciidoctor</td>
|
299
|
+
<td>ad, adoc, asciidoc</td>
|
300
|
+
</tr>
|
301
|
+
<tr>
|
302
|
+
<td>Builder</td>
|
303
|
+
<td>builder</td>
|
304
|
+
</tr>
|
305
|
+
<tr>
|
306
|
+
<td>CSV</td>
|
307
|
+
<td>rcsv</td>
|
308
|
+
</tr>
|
309
|
+
<tr>
|
310
|
+
<td>CoffeeScript</td>
|
311
|
+
<td>coffee</td>
|
312
|
+
</tr>
|
313
|
+
<tr>
|
314
|
+
<td>WikiCloth</td>
|
315
|
+
<td>wiki, mediawiki, mw</td>
|
316
|
+
</tr>
|
317
|
+
<tr>
|
318
|
+
<td>Creole</td>
|
319
|
+
<td>wiki, creole</td>
|
320
|
+
</tr>
|
321
|
+
<tr>
|
322
|
+
<td>Etanni</td>
|
323
|
+
<td>etn, etanni</td>
|
324
|
+
</tr>
|
325
|
+
<tr>
|
326
|
+
<td>Haml</td>
|
327
|
+
<td>haml</td>
|
328
|
+
</tr>
|
329
|
+
<tr>
|
330
|
+
<td>Less</td>
|
331
|
+
<td>less</td>
|
332
|
+
</tr>
|
333
|
+
<tr>
|
334
|
+
<td>Liquid</td>
|
335
|
+
<td>liquid</td>
|
336
|
+
</tr>
|
337
|
+
<tr>
|
338
|
+
<td>Markaby</td>
|
339
|
+
<td>mab</td>
|
340
|
+
</tr>
|
341
|
+
<tr>
|
342
|
+
<td>Nokogiri</td>
|
343
|
+
<td>nokogiri</td>
|
344
|
+
</tr>
|
345
|
+
<tr>
|
346
|
+
<td>Plain</td>
|
347
|
+
<td>html</td>
|
348
|
+
</tr>
|
349
|
+
<tr>
|
350
|
+
<td>RDoc</td>
|
351
|
+
<td>rdoc</td>
|
352
|
+
</tr>
|
353
|
+
<tr>
|
354
|
+
<td>Radius</td>
|
355
|
+
<td>radius</td>
|
356
|
+
</tr>
|
357
|
+
<tr>
|
358
|
+
<td>RedCloth</td>
|
359
|
+
<td>textile</td>
|
360
|
+
</tr>
|
361
|
+
<tr>
|
362
|
+
<td>Sass</td>
|
363
|
+
<td>sass</td>
|
364
|
+
</tr>
|
365
|
+
<tr>
|
366
|
+
<td>Scss</td>
|
367
|
+
<td>scss</td>
|
368
|
+
</tr>
|
369
|
+
<tr>
|
370
|
+
<td>Slim</td>
|
371
|
+
<td>slim</td>
|
372
|
+
</tr>
|
373
|
+
<tr>
|
374
|
+
<td>String</td>
|
375
|
+
<td>str</td>
|
376
|
+
</tr>
|
377
|
+
<tr>
|
378
|
+
<td>Yajl</td>
|
379
|
+
<td>yajl</td>
|
380
|
+
</tr>
|
381
|
+
</table>
|
382
|
+
|
383
|
+
### Root
|
384
|
+
|
385
|
+
Template lookup is performed under the `Hanami::View.configuration.root` directory. You can specify a different path on a per view basis:
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
class ViewWithDifferentRoot
|
389
|
+
include Hanami::View
|
390
|
+
|
391
|
+
root 'path/to/root'
|
392
|
+
end
|
393
|
+
```
|
394
|
+
|
395
|
+
### Template
|
396
|
+
|
397
|
+
The template file must be located under the relevant `root` and must match the class name:
|
398
|
+
|
399
|
+
```ruby
|
400
|
+
puts Hanami::View.configuration.root # => #<Pathname:app/templates>
|
401
|
+
Articles::Index.template # => "articles/index"
|
402
|
+
```
|
403
|
+
|
404
|
+
Each view can specify a different template:
|
405
|
+
|
406
|
+
```ruby
|
407
|
+
module Articles
|
408
|
+
class Create
|
409
|
+
include Hanami::View
|
410
|
+
|
411
|
+
template 'articles/new'
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
Articles::Create.template # => "articles/new"
|
416
|
+
```
|
417
|
+
|
418
|
+
### Partials
|
419
|
+
|
420
|
+
Partials can be rendered within a template:
|
421
|
+
|
422
|
+
```erb
|
423
|
+
<%= render partial: 'articles/form', locals: { secret: 23 } %>
|
424
|
+
```
|
425
|
+
|
426
|
+
It will look for a template `articles/_form.html.erb` and make available both the view's and partial's locals (eg. `article` and `secret`).
|
427
|
+
|
428
|
+
### Templates
|
429
|
+
|
430
|
+
Templates can be rendered within another template:
|
431
|
+
|
432
|
+
```erb
|
433
|
+
<%= render template: 'articles/new', locals: { errors: {} } %>
|
434
|
+
```
|
435
|
+
|
436
|
+
It will render `articles/new.html.erb` and make available both the view's and templates's locals (eg. `article` and `errors`).
|
437
|
+
|
438
|
+
### Layouts
|
439
|
+
|
440
|
+
Layouts are wrappers for views. Layouts may serve to reuse common markup.
|
441
|
+
|
442
|
+
```ruby
|
443
|
+
class ApplicationLayout
|
444
|
+
include Hanami::Layout
|
445
|
+
|
446
|
+
def page_title
|
447
|
+
'Title:'
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
module Articles
|
452
|
+
class Index
|
453
|
+
include Hanami::View
|
454
|
+
layout :application
|
455
|
+
|
456
|
+
def page_title
|
457
|
+
"#{ layout.page_title } articles"
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
class RssIndex < Index
|
462
|
+
format :rss
|
463
|
+
layout false
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
Articles::Index.render(format: :html) # => Will use ApplicationLayout
|
468
|
+
Articles::Index.render(format: :rss) # => Will use nothing
|
469
|
+
```
|
470
|
+
|
471
|
+
As per convention, layout templates are located under `Hanami::View.root` or `ApplicationLayout.root` and use the underscored name (eg. `ApplicationLayout => application.html.erb`).
|
472
|
+
|
473
|
+
### Optional Content
|
474
|
+
|
475
|
+
If we want to render optional contents such as sidebar links or page specific javascripts, we can use `#content`
|
476
|
+
It accepts a key that represents a method that should be available within the rendering context.
|
477
|
+
That context is made of the locals, and the methods that view and layout respond to.
|
478
|
+
If the context can't dispatch that method, it returns `nil`.
|
479
|
+
|
480
|
+
Given the following layout template.
|
26
481
|
|
27
|
-
|
482
|
+
```erb
|
483
|
+
<!doctype HTML>
|
484
|
+
<html>
|
485
|
+
<!-- ... -->
|
486
|
+
<body>
|
487
|
+
<!-- ... -->
|
488
|
+
<%= content :footer %>
|
489
|
+
</body>
|
490
|
+
</html>
|
491
|
+
```
|
492
|
+
|
493
|
+
We have two views, one responds to `#footer` (`Products::Show`) and the other doesn't (`Products::Index`).
|
494
|
+
When the first is rendered, `content` gives back the returning value of `#footer`.
|
495
|
+
In the other case, `content` returns `nil`.
|
496
|
+
|
497
|
+
```ruby
|
498
|
+
module Products
|
499
|
+
class Index
|
500
|
+
include Hanami::View
|
501
|
+
end
|
502
|
+
|
503
|
+
class Show
|
504
|
+
include Hanami::View
|
505
|
+
|
506
|
+
def footer
|
507
|
+
"contents for footer"
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
```
|
512
|
+
|
513
|
+
### Presenters
|
514
|
+
|
515
|
+
The goal of a presenter is to wrap and reuse presentational logic for an object.
|
516
|
+
|
517
|
+
```ruby
|
518
|
+
class Map
|
519
|
+
attr_reader :locations
|
520
|
+
|
521
|
+
def initialize(locations)
|
522
|
+
@locations = locations
|
523
|
+
end
|
524
|
+
|
525
|
+
def location_names
|
526
|
+
@locations.join(', ')
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
class MapPresenter
|
531
|
+
include Hanami::Presenter
|
532
|
+
|
533
|
+
def count
|
534
|
+
locations.count
|
535
|
+
end
|
536
|
+
|
537
|
+
def location_names
|
538
|
+
super.upcase
|
539
|
+
end
|
540
|
+
|
541
|
+
def inspect_object
|
542
|
+
@object.inspect
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
map = Map.new(['Rome', 'Boston'])
|
547
|
+
presenter = MapPresenter.new(map)
|
548
|
+
|
549
|
+
# access a map method
|
550
|
+
puts presenter.locations # => ['Rome', 'Boston']
|
551
|
+
|
552
|
+
# access presenter concrete methods
|
553
|
+
puts presenter.count # => 2
|
554
|
+
|
555
|
+
# uses super to access original object implementation
|
556
|
+
puts presenter.location_names # => 'ROME, BOSTON'
|
557
|
+
|
558
|
+
# it has private access to the original object
|
559
|
+
puts presenter.inspect_object # => #<Map:0x007fdeada0b2f0 @locations=["Rome", "Boston"]>
|
560
|
+
```
|
561
|
+
|
562
|
+
### Configuration
|
563
|
+
|
564
|
+
__Hanami::View__ can be configured with a DSL that determines its behavior.
|
565
|
+
It supports a few options:
|
566
|
+
|
567
|
+
```ruby
|
568
|
+
require 'hanami/view'
|
569
|
+
|
570
|
+
Hanami::View.configure do
|
571
|
+
# Set the root path where to search for templates
|
572
|
+
# Argument: String, Pathname, #to_pathname, defaults to the current directory
|
573
|
+
#
|
574
|
+
root '/path/to/root'
|
575
|
+
|
576
|
+
# Default encoding for templates
|
577
|
+
# Argument: String, defaults to utf-8
|
578
|
+
#
|
579
|
+
default_encoding 'koi-8'
|
580
|
+
|
581
|
+
# Set the Ruby namespace where to lookup for views
|
582
|
+
# Argument: Class, Module, String, defaults to Object
|
583
|
+
#
|
584
|
+
namespace 'MyApp::Views'
|
585
|
+
|
586
|
+
# Set the global layout
|
587
|
+
# Argument: Symbol, defaults to nil
|
588
|
+
#
|
589
|
+
layout :application
|
590
|
+
|
591
|
+
# Set modules that you want to include in all views
|
592
|
+
# Argument: Block
|
593
|
+
#
|
594
|
+
prepare do
|
595
|
+
include MyCustomModule
|
596
|
+
before { do_something }
|
597
|
+
end
|
598
|
+
end
|
599
|
+
```
|
600
|
+
|
601
|
+
All those global configurations can be overwritten at a finer grained level:
|
602
|
+
views. Each view and layout has its own copy of the global configuration, so
|
603
|
+
that changes are inherited from the top to the bottom, but not bubbled up in the
|
604
|
+
opposite direction.
|
605
|
+
|
606
|
+
```ruby
|
607
|
+
require 'hanami/view'
|
608
|
+
|
609
|
+
Hanami::View.configure do
|
610
|
+
root '/path/to/root'
|
611
|
+
end
|
612
|
+
|
613
|
+
class Show
|
614
|
+
include Hanami::View
|
615
|
+
root '/another/root'
|
616
|
+
end
|
617
|
+
|
618
|
+
Hanami::View.configuration.root # => #<Pathname:/path/to/root>
|
619
|
+
Show.root # => #<Pathname:/another/root>
|
620
|
+
```
|
621
|
+
|
622
|
+
### Reusability
|
623
|
+
|
624
|
+
__Hanami::View__ can be used as a singleton framework as seen in this README.
|
625
|
+
The application code includes `Hanami::View` or `Hanami::Layout` directly
|
626
|
+
and the configuration is unique per Ruby process.
|
627
|
+
|
628
|
+
While this is convenient for tiny applications, it doesn't fit well for more
|
629
|
+
complex scenarios, where we want micro applications to coexist together.
|
630
|
+
|
631
|
+
```ruby
|
632
|
+
require 'hanami/view'
|
633
|
+
|
634
|
+
Hanami::View.configure do
|
635
|
+
root '/path/to/root'
|
636
|
+
end
|
637
|
+
|
638
|
+
module WebApp
|
639
|
+
View = Hanami::View.duplicate(self)
|
640
|
+
end
|
641
|
+
|
642
|
+
module ApiApp
|
643
|
+
View = Hanami::View.duplicate(self) do
|
644
|
+
root '/another/root'
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
Hanami::View.configuration.root # => #<Pathname:/path/to/root>
|
649
|
+
WebApp::View.configuration.root # => #<Pathname:/path/to/root>, inherited from Hanami::View
|
650
|
+
ApiApp::View.configuration.root # => #<Pathname:/another/root>
|
651
|
+
```
|
652
|
+
|
653
|
+
The code above defines `WebApp::View` and `WebApp::Layout`, to be used for
|
654
|
+
the `WebApp` views, while `ApiApp::View` and `ApiApp::Layout` have a different
|
655
|
+
configuration.
|
656
|
+
|
657
|
+
### Thread safety
|
658
|
+
|
659
|
+
__Hanami::View__ is thread safe during the runtime, but it isn't during the loading process.
|
660
|
+
Please load the framework as the last thing before your application starts.
|
661
|
+
Also, be sure that your app provides a thread safe context while it's loaded.
|
662
|
+
|
663
|
+
|
664
|
+
```ruby
|
665
|
+
Mutex.new.synchronize do
|
666
|
+
Hanami::View.load!
|
667
|
+
end
|
668
|
+
```
|
669
|
+
|
670
|
+
After this operation, all the class variables are frozen, in order to prevent accidental modifications at the run time.
|
671
|
+
|
672
|
+
**This is not necessary, when Hanami::View is used within a Hanami application.**
|
673
|
+
|
674
|
+
### Security
|
28
675
|
|
29
|
-
|
676
|
+
The output of views and presenters is always **autoescaped**.
|
30
677
|
|
31
|
-
|
678
|
+
**ATTENTION:** In order to prevent XSS attacks, please read the instructions below.
|
679
|
+
Because Hanami::View supports a lot of template engines, the escape happens at the level of the view.
|
680
|
+
Most of the time everything happens automatically, but there are still some corner cases that need your manual intervention.
|
681
|
+
|
682
|
+
#### View autoescape
|
683
|
+
|
684
|
+
```ruby
|
685
|
+
require 'hanami/view'
|
686
|
+
|
687
|
+
User = Struct.new(:name)
|
688
|
+
|
689
|
+
module Users
|
690
|
+
class Show
|
691
|
+
include Hanami::View
|
692
|
+
|
693
|
+
def user_name
|
694
|
+
user.name
|
695
|
+
end
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
# ERB template
|
700
|
+
# <div id="user_name"><%= user_name %></div>
|
701
|
+
|
702
|
+
user = User.new("<script>alert('xss')</script>")
|
703
|
+
|
704
|
+
# THIS IS USEFUL FOR UNIT TESTING:
|
705
|
+
template = Hanami::View::Template.new('users/show.html.erb')
|
706
|
+
view = Users::Show.new(template, user: user)
|
707
|
+
view.user_name # => "<script>alert('xss')</script>"
|
708
|
+
|
709
|
+
# THIS IS THE RENDERING OUTPUT:
|
710
|
+
Users::Show.render(format: :html, user: user)
|
711
|
+
# => <div id="user_name"><script>alert('xss')</script></div>
|
712
|
+
```
|
713
|
+
|
714
|
+
#### Presenter autoescape
|
715
|
+
|
716
|
+
```ruby
|
717
|
+
require 'hanami/view'
|
718
|
+
|
719
|
+
User = Struct.new(:name)
|
720
|
+
|
721
|
+
class UserPresenter
|
722
|
+
include Hanami::Presenter
|
723
|
+
end
|
724
|
+
|
725
|
+
user = User.new("<script>alert('xss')</script>")
|
726
|
+
presenter = UserPresenter.new(user)
|
727
|
+
|
728
|
+
presenter.name # => "<script>alert('xss')</script>"
|
729
|
+
```
|
730
|
+
|
731
|
+
#### Escape entire objects
|
732
|
+
|
733
|
+
We have seen that concrete methods in views are automatically escaped.
|
734
|
+
This is great, but tedious if you need to print a lot of information from a given object.
|
735
|
+
|
736
|
+
Imagine you have `user` as part of the view locals.
|
737
|
+
If you want to use `<%= user.name %>` directly, **you're still vulnerable to XSS attacks**.
|
738
|
+
|
739
|
+
You have two alternatives:
|
740
|
+
|
741
|
+
* To use a concrete presenter (eg. `UserPresenter`)
|
742
|
+
* Escape the entire object (see the example below)
|
743
|
+
|
744
|
+
Both those solutions allow you to keep the template syntax unchanged, but to have a safer output.
|
745
|
+
|
746
|
+
```ruby
|
747
|
+
require 'hanami/view'
|
748
|
+
|
749
|
+
User = Struct.new(:first_name, :last_name)
|
750
|
+
|
751
|
+
module Users
|
752
|
+
class Show
|
753
|
+
include Hanami::View
|
754
|
+
|
755
|
+
def user
|
756
|
+
_escape locals[:user]
|
757
|
+
end
|
758
|
+
end
|
759
|
+
end
|
760
|
+
|
761
|
+
# ERB template:
|
762
|
+
#
|
763
|
+
# <div id="first_name">
|
764
|
+
# <%= user.first_name %>
|
765
|
+
# </div>
|
766
|
+
# <div id="last_name">
|
767
|
+
# <%= user.last_name %>
|
768
|
+
# </div>
|
769
|
+
|
770
|
+
first_name = "<script>alert('first_name')</script>"
|
771
|
+
last_name = "<script>alert('last_name')</script>"
|
772
|
+
|
773
|
+
user = User.new(first_name, last_name)
|
774
|
+
html = Users::Show.render(format: :html, user: user)
|
775
|
+
|
776
|
+
html
|
777
|
+
# =>
|
778
|
+
# <div id="first_name">
|
779
|
+
# <script>alert('first_name')</script>
|
780
|
+
# </div>
|
781
|
+
# <div id="last_name">
|
782
|
+
# <script>alert('last_name')</script>
|
783
|
+
# </div>
|
784
|
+
```
|
785
|
+
|
786
|
+
#### Raw contents
|
787
|
+
|
788
|
+
You can use `_raw` to mark an output as safe.
|
789
|
+
Please note that **this may open your application to XSS attacks.**
|
790
|
+
|
791
|
+
#### Raw contents in views
|
792
|
+
|
793
|
+
```ruby
|
794
|
+
require 'hanami/view'
|
795
|
+
|
796
|
+
User = Struct.new(:name)
|
797
|
+
|
798
|
+
module Users
|
799
|
+
class Show
|
800
|
+
include Hanami::View
|
801
|
+
|
802
|
+
def user_name
|
803
|
+
_raw user.name
|
804
|
+
end
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
808
|
+
# ERB template
|
809
|
+
# <div id="user_name"><%= user_name %></div>
|
810
|
+
|
811
|
+
user = User.new("<script>alert('xss')</script>")
|
812
|
+
html = Users::Show.render(format: :html, user: user)
|
813
|
+
|
814
|
+
html
|
815
|
+
# => <div id="user_name"><script>alert('xss')</script></div>
|
816
|
+
```
|
817
|
+
|
818
|
+
#### Raw contents in presenters
|
819
|
+
|
820
|
+
```ruby
|
821
|
+
require 'hanami/view'
|
822
|
+
|
823
|
+
User = Struct.new(:name)
|
824
|
+
|
825
|
+
class UserPresenter
|
826
|
+
include Hanami::Presenter
|
827
|
+
|
828
|
+
def first_name
|
829
|
+
_raw @object.first_name
|
830
|
+
end
|
831
|
+
end
|
832
|
+
|
833
|
+
user = User.new("<script>alert('xss')</script>")
|
834
|
+
presenter = UserPresenter.new(user)
|
835
|
+
|
836
|
+
presenter.name # => "<script>alert('xss')</script>"
|
837
|
+
```
|
838
|
+
|
839
|
+
## Versioning
|
840
|
+
|
841
|
+
__Hanami::View__ uses [Semantic Versioning 2.0.0](http://semver.org)
|
32
842
|
|
33
843
|
## Contributing
|
34
844
|
|
35
|
-
|
845
|
+
1. Fork it
|
846
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
847
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
848
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
849
|
+
5. Create new Pull Request
|
850
|
+
|
851
|
+
## Copyright
|
852
|
+
|
853
|
+
Copyright 2014-2016 Luca Guidi – Released under MIT License
|
36
854
|
|
855
|
+
This project was formerly known as Lotus (`lotus-view`).
|