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