viewlet 0.0.1
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.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/MIT.LICENSE +20 -0
- data/README.md +206 -0
- data/Rakefile +1 -0
- data/examples/bootstrap_tabs/bootstrap_tabs.html.haml +17 -0
- data/examples/bootstrap_tabs/plugin.js +1 -0
- data/examples/bootstrap_tabs/usage.html.haml +8 -0
- data/lib/viewlet/base.rb +48 -0
- data/lib/viewlet/helpers.rb +15 -0
- data/lib/viewlet/railtie.rb +11 -0
- data/lib/viewlet/template.rb +31 -0
- data/lib/viewlet/version.rb +3 -0
- data/lib/viewlet.rb +7 -0
- data/viewlet.gemspec +22 -0
- metadata +82 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/MIT.LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Dmitriy Kalinin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
## Goals
|
2
|
+
|
3
|
+
* to ease creation of view components
|
4
|
+
|
5
|
+
Problem: Most likely your site has few similar view structures that
|
6
|
+
are repeated throughout the site (e.g. *list* of members, groups, etc.).
|
7
|
+
One solution is to refactor such code into a shared partial
|
8
|
+
(may be `_list_section.html.haml`) and pass customization options
|
9
|
+
via locals hash; however, with this approach it can become quite
|
10
|
+
challenging/inelegant to apply customizations.
|
11
|
+
|
12
|
+
* to organize HTML/JS/CSS files based on a feature rather than file type
|
13
|
+
|
14
|
+
Problem: As soon as you start extracting reusable view components
|
15
|
+
from your pages it becomes weird to have HTML/CSS/JS component files
|
16
|
+
spread out in three different directories. Turning your component into a
|
17
|
+
gem remedies that problem since gems can have separate `assets`
|
18
|
+
directory; however, I don't see a benefit in making every single
|
19
|
+
component into a gem, especially when it's application specific.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem "viewlet", :git => "https://github.com/cppforlife/viewlet.git"
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
Let's say we have `GroupsController#show` that lists group members.
|
30
|
+
Here is how `show.html.haml` could look:
|
31
|
+
|
32
|
+
```haml
|
33
|
+
%h1= "Group: #{@group.name}"
|
34
|
+
%p= @group.description
|
35
|
+
|
36
|
+
= viewlet(:list_section) do |s|
|
37
|
+
- s.heading "Group members"
|
38
|
+
- s.empty_description "No members in this group"
|
39
|
+
|
40
|
+
- s.collapse_button false
|
41
|
+
- s.add_button do
|
42
|
+
= link_to "Invite Members", new_group_member_path(@group)
|
43
|
+
|
44
|
+
- s.items @group.members
|
45
|
+
|
46
|
+
- s.row_title do |member|
|
47
|
+
.name= member.name
|
48
|
+
.summary= member.summary
|
49
|
+
|
50
|
+
- s.row_details do |member|
|
51
|
+
= render :partial => "some_other_partial", :locals => {:member => member}
|
52
|
+
```
|
53
|
+
|
54
|
+
Now let's define list_section viewlet. Viewlets live in `app/viewlets`
|
55
|
+
and each one must have at least `<name>.html.haml`.
|
56
|
+
|
57
|
+
In `app/viewlets/list_section/list_section.html.haml`:
|
58
|
+
|
59
|
+
```haml
|
60
|
+
.list_section
|
61
|
+
%h2
|
62
|
+
= heading
|
63
|
+
|
64
|
+
- if add_button
|
65
|
+
%small= add_button
|
66
|
+
|
67
|
+
- if collapse_button
|
68
|
+
%small.collapse_button= link_to "Collapse", "#"
|
69
|
+
|
70
|
+
- if items.empty?
|
71
|
+
- # outputs value regardless being defined as an argument-less block or a plain value
|
72
|
+
%p= empty_description
|
73
|
+
|
74
|
+
- else
|
75
|
+
%ul
|
76
|
+
- items.each do |item|
|
77
|
+
%li{:class => cycle("odd", "even", :name => :list_section)}
|
78
|
+
.left= list_section.row_title(item)
|
79
|
+
|
80
|
+
- # alternative way of capturing block's content
|
81
|
+
.right= capture(item, &row_details)
|
82
|
+
```
|
83
|
+
|
84
|
+
All viewlet options (heading, add_button, etc.) set in `show.html.haml`
|
85
|
+
become available in `list_section.html.haml` as local variables. None of
|
86
|
+
those options are special and you can make up as many as you want.
|
87
|
+
|
88
|
+
Note: If there aren't CSS or JS files you want to keep next to your viewlet
|
89
|
+
HTML file you don't need to create a directory for each viewlet; simply
|
90
|
+
put them in `app/viewlets` e.g. `app/viewlets/list_section.html.haml`.
|
91
|
+
|
92
|
+
### CSS & JS
|
93
|
+
|
94
|
+
You can also add other types of files to `app/viewlets/list_section/`.
|
95
|
+
Idea here is that your viewlet is self-contained and
|
96
|
+
encapsulates all needed parts - HTML, CSS, and JS.
|
97
|
+
|
98
|
+
In `app/viewlets/list_section/plugin.css.scss`:
|
99
|
+
|
100
|
+
```scss
|
101
|
+
.list_section {
|
102
|
+
width: 300px;
|
103
|
+
|
104
|
+
ul {
|
105
|
+
margin: 0;
|
106
|
+
}
|
107
|
+
|
108
|
+
li {
|
109
|
+
border: 1px solid #ccc;
|
110
|
+
margin-bottom: -1px;
|
111
|
+
padding: 10px;
|
112
|
+
list-style-type: none;
|
113
|
+
overflow: hidden;
|
114
|
+
}
|
115
|
+
|
116
|
+
.left {
|
117
|
+
float: left;
|
118
|
+
}
|
119
|
+
|
120
|
+
.right {
|
121
|
+
float: right;
|
122
|
+
}
|
123
|
+
}
|
124
|
+
```
|
125
|
+
|
126
|
+
To include list_section viewlet CSS in your application add
|
127
|
+
|
128
|
+
*= require list_section/plugin
|
129
|
+
|
130
|
+
to your `application.css`
|
131
|
+
|
132
|
+
In `app/viewlets/list_section/plugin.js`:
|
133
|
+
|
134
|
+
```javascript
|
135
|
+
// Probably define listSection() jQuery plugin
|
136
|
+
```
|
137
|
+
|
138
|
+
To include list_section viewlet JS in your application add
|
139
|
+
|
140
|
+
```javascript
|
141
|
+
//= require list_section/plugin
|
142
|
+
```
|
143
|
+
|
144
|
+
to your `application.js`
|
145
|
+
|
146
|
+
## Misc
|
147
|
+
|
148
|
+
* Let's say we decide to make our list_section viewlet use
|
149
|
+
third-party list re-ordering library (e.g. `orderable-list.js`).
|
150
|
+
You can add `orderable-list.js` javascript file to
|
151
|
+
`app/viewlets/list_section` and require it from `plugin.js`:
|
152
|
+
|
153
|
+
```javascript
|
154
|
+
//= require ./orderable-list
|
155
|
+
```
|
156
|
+
|
157
|
+
* Let's say our `plugin.js` defined jQuery plugin `listSection`
|
158
|
+
so that in our `application.js` we can do something like this:
|
159
|
+
|
160
|
+
```javascript
|
161
|
+
$(document).ready(function(){
|
162
|
+
$(".list_section").listSection();
|
163
|
+
});
|
164
|
+
```
|
165
|
+
|
166
|
+
This is fine; however, that means that our component is not
|
167
|
+
really functional until we add that javascript piece somewhere.
|
168
|
+
Alternatively you can put it right after HTML so everytime
|
169
|
+
list_section is rendered it will be automatically initialized.
|
170
|
+
|
171
|
+
For example in `list_section.html.haml`:
|
172
|
+
|
173
|
+
```haml
|
174
|
+
.list_section{:id => unique_id}
|
175
|
+
%h2= heading
|
176
|
+
...
|
177
|
+
|
178
|
+
- unless defined?(no_script)
|
179
|
+
:javascript
|
180
|
+
$(document).ready(function(){
|
181
|
+
$("##{unique_id}").listSection();
|
182
|
+
});
|
183
|
+
```
|
184
|
+
|
185
|
+
Every viewlet has a predefined local variable `unique_id`
|
186
|
+
that could be used as HTML id.
|
187
|
+
|
188
|
+
* It's trivial to subclass `Viewlet::Base` to add new functionality.
|
189
|
+
`class_name` option lets you set custom viewlet class:
|
190
|
+
|
191
|
+
```haml
|
192
|
+
= viewlet(:list_section, :class_name => "CustomListSectionViewlet") do
|
193
|
+
...
|
194
|
+
```
|
195
|
+
|
196
|
+
* You do not have to pass in block to `viewlet`:
|
197
|
+
|
198
|
+
```haml
|
199
|
+
= viewlet(:password_strength)
|
200
|
+
```
|
201
|
+
|
202
|
+
## Todo
|
203
|
+
|
204
|
+
* come up with a better name for main files - *plugin* doesn't sound that good
|
205
|
+
* `lib/viewlets/` as fallback viewlet lookup path
|
206
|
+
* automatically load custom Viewlet::Base subclass from `some_viewlet/plugin.rb`
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
.tabbable{:id => unique_id}
|
2
|
+
%ul.nav.nav-tabs
|
3
|
+
- tabs.each_with_index do |tab, i|
|
4
|
+
%li{:class => ("active" if i == 0)}
|
5
|
+
= link_to tab, "#tab#{i}"
|
6
|
+
|
7
|
+
.tab-content
|
8
|
+
- tabs.each_with_index do |tab, i|
|
9
|
+
.tab-pane{:id => "tab#{i}", :class => ("active" if i == 0)}
|
10
|
+
%h1= tab
|
11
|
+
= send(tab.underscore)
|
12
|
+
|
13
|
+
- unless defined?(no_script)
|
14
|
+
javascript:
|
15
|
+
$(document).ready(function(){
|
16
|
+
$("##{unique_id}").tab();
|
17
|
+
});
|
@@ -0,0 +1 @@
|
|
1
|
+
// http://twitter.github.com/bootstrap/assets/js/bootstrap-tab.js
|
data/lib/viewlet/base.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "viewlet/template"
|
2
|
+
|
3
|
+
module Viewlet
|
4
|
+
class Base
|
5
|
+
def initialize(name, view)
|
6
|
+
@name = name
|
7
|
+
@view = view
|
8
|
+
@variables = {
|
9
|
+
@name.to_sym => self,
|
10
|
+
:unique_id => "viewlet_#{rand(36**20).to_s(36)}"
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def render
|
15
|
+
Template.find(@name.to_s).render(@view, @variables)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def method_missing(method, *args, &block)
|
21
|
+
is_write_op = if @variables[method].is_a?(Proc)
|
22
|
+
block.present?
|
23
|
+
else
|
24
|
+
args.any? || block.present?
|
25
|
+
end
|
26
|
+
|
27
|
+
send("_#{is_write_op ? :write : :read}_variable", method, *args, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def _read_variable(name, *args, &block)
|
31
|
+
if @variables[name].is_a?(Proc)
|
32
|
+
@view.capture(*args, &@variables[name])
|
33
|
+
else
|
34
|
+
@variables[name]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def _write_variable(name, *args, &block)
|
39
|
+
@variables[name] = if block
|
40
|
+
# HAML changes argument-less block {|| } into block {|*a| }
|
41
|
+
# which makes block.arity to be -1 instead of just 0
|
42
|
+
block.arity == -1 ? @view.capture(&block) : block
|
43
|
+
else
|
44
|
+
args.first
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Viewlet
|
2
|
+
module Helpers
|
3
|
+
def viewlet(name, options={}, &block)
|
4
|
+
klass = options[:class_name].try(:constantize) || Base
|
5
|
+
viewlet = klass.new(name, self)
|
6
|
+
|
7
|
+
case block.arity
|
8
|
+
when 0 then block.call
|
9
|
+
when 1 then block.call(viewlet)
|
10
|
+
end if block_given?
|
11
|
+
|
12
|
+
viewlet.render
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Viewlet
|
2
|
+
class Railtie < ::Rails::Railtie
|
3
|
+
initializer "viewlets.view_helpers" do
|
4
|
+
ActionView::Base.send :include, Viewlet::Helpers
|
5
|
+
end
|
6
|
+
|
7
|
+
config.to_prepare do |app|
|
8
|
+
Rails.application.config.assets.paths << Rails.root.join("app", "viewlets")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Viewlet
|
2
|
+
class Template
|
3
|
+
def self.find(name)
|
4
|
+
args = [name, "", false, {:locale => [:en], :formats => [:html], :handlers => [:erb, :haml]}, nil]
|
5
|
+
template = path_resolver.find_all(*args).first ||
|
6
|
+
raise(ActionView::MissingTemplate.new([path_resolver], *args))
|
7
|
+
|
8
|
+
# Cannot refresh template because it will try to use
|
9
|
+
# view's lookup context which will not include app/viewlets dir
|
10
|
+
template.virtual_path = nil
|
11
|
+
|
12
|
+
new(template)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(template)
|
16
|
+
@template = template
|
17
|
+
end
|
18
|
+
|
19
|
+
def render(view, variables={})
|
20
|
+
@template.locals = variables.keys
|
21
|
+
@template.render(view, variables)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def self.path_resolver
|
27
|
+
ActionView::FileSystemResolver.new \
|
28
|
+
Rails.root.join("app/viewlets"), "{:action/,}:action{.:locale,}{.:formats,}{.:handlers,}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/viewlet.rb
ADDED
data/viewlet.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "viewlet/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "viewlet"
|
7
|
+
s.version = Viewlet::VERSION
|
8
|
+
s.authors = ["Dmitriy Kalinin"]
|
9
|
+
s.email = ["cppforlife@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/cppforlife/viewlet"
|
11
|
+
s.summary = "Rails view components"
|
12
|
+
s.description = s.summary
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
# specify any dependencies here; for example:
|
20
|
+
# s.add_development_dependency "rspec"
|
21
|
+
# s.add_runtime_dependency "rest-client"
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: viewlet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Dmitriy Kalinin
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-07-20 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Rails view components
|
23
|
+
email:
|
24
|
+
- cppforlife@gmail.com
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- Gemfile
|
34
|
+
- MIT.LICENSE
|
35
|
+
- README.md
|
36
|
+
- Rakefile
|
37
|
+
- examples/bootstrap_tabs/bootstrap_tabs.html.haml
|
38
|
+
- examples/bootstrap_tabs/plugin.js
|
39
|
+
- examples/bootstrap_tabs/usage.html.haml
|
40
|
+
- lib/viewlet.rb
|
41
|
+
- lib/viewlet/base.rb
|
42
|
+
- lib/viewlet/helpers.rb
|
43
|
+
- lib/viewlet/railtie.rb
|
44
|
+
- lib/viewlet/template.rb
|
45
|
+
- lib/viewlet/version.rb
|
46
|
+
- viewlet.gemspec
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: https://github.com/cppforlife/viewlet
|
49
|
+
licenses: []
|
50
|
+
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 3
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
version: "0"
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 1.3.7
|
78
|
+
signing_key:
|
79
|
+
specification_version: 3
|
80
|
+
summary: Rails view components
|
81
|
+
test_files: []
|
82
|
+
|