viewlet 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ .DS_Store
4
+ Gemfile.lock
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in viewlet.gemspec
4
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ = viewlet(:bootstrap_tabs) do |t|
2
+ - t.tabs %w(Movies Books)
3
+
4
+ - t.movies do
5
+ %p Bunch of movies
6
+
7
+ - t.books do
8
+ %p Bunch of books
@@ -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
@@ -0,0 +1,3 @@
1
+ module Viewlet
2
+ VERSION = "0.0.1"
3
+ end
data/lib/viewlet.rb ADDED
@@ -0,0 +1,7 @@
1
+ require "viewlet/version"
2
+ require "viewlet/base"
3
+ require "viewlet/helpers"
4
+ require "viewlet/railtie"
5
+
6
+ module Viewlet
7
+ end
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
+