components 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +6 -0
- data/MIT-LICENSE +20 -0
- data/README.md +148 -0
- data/Rakefile +42 -0
- data/VERSION.yml +4 -0
- data/components.gemspec +80 -0
- data/generators/component/component_generator.rb +13 -0
- data/generators/component/templates/component_template.rb +7 -0
- data/generators/component/templates/view_template.rb +0 -0
- data/lib/components.rb +92 -0
- data/lib/components/base.rb +126 -0
- data/lib/components/caching.rb +187 -0
- data/lib/components/helpers.rb +0 -0
- data/lib/components/view.rb +13 -0
- data/rails/init.rb +3 -0
- data/test/action_controller_test.rb +24 -0
- data/test/app/components/child/one.erb +1 -0
- data/test/app/components/child_component.rb +5 -0
- data/test/app/components/hello_world/bolded.erb +1 -0
- data/test/app/components/hello_world/say_it_with_help.erb +1 -0
- data/test/app/components/hello_world_component.rb +27 -0
- data/test/app/components/parent/one.erb +1 -0
- data/test/app/components/parent/two.erb +1 -0
- data/test/app/components/parent_component.rb +9 -0
- data/test/app/components/rich_view/form.erb +3 -0
- data/test/app/components/rich_view/linker.erb +1 -0
- data/test/app/components/rich_view/urler.erb +1 -0
- data/test/app/components/rich_view_component.rb +14 -0
- data/test/app/controllers/application_controller.rb +0 -0
- data/test/app/controllers/hello_world_controller.rb +5 -0
- data/test/caching_test.rb +60 -0
- data/test/components_test.rb +52 -0
- data/test/test_helper.rb +24 -0
- metadata +98 -0
data/.document
ADDED
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Lance Ivy
|
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,148 @@
|
|
1
|
+
## Components
|
2
|
+
|
3
|
+
This plugin attempts to implement components in the simplest, cleanest, fastest way possible. Inspired by the Cells plugin (http://cells.rubyforge.org) by Nick Sutterer and Peter Bex.
|
4
|
+
|
5
|
+
A component can be thought of as a very lightweight controller with supporting view templates. The difference between a component "controller" and a Rails controller is that the component controller's methods are very much normal methods - they accept arguments, and they return a string. There is no magical auto-rendering, and there is no access to data that is not a) in the arguments or b) in the database. For example, there is no access to the request, the request parameters, or the session. This is designed to encourage good design and reuse of components, and to ensure that they don't take over the request/response job of your Rails controllers.
|
6
|
+
|
7
|
+
Speaking imprecisely, components prepare for and then render templates.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
Note that these examples are very simplistic and would be better implemented using Rails partials.
|
12
|
+
|
13
|
+
### Generator
|
14
|
+
|
15
|
+
Running `script/generator users details` will create a UsersComponent with a "details" view. You might then flesh out the templates like this:
|
16
|
+
|
17
|
+
class UsersComponent < Components::Base
|
18
|
+
def details(user_or_id)
|
19
|
+
@user = user_or_id.is_a?(User) ? user_or_id : User.find(user_or_id)
|
20
|
+
render
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
### From ActionController
|
25
|
+
|
26
|
+
class UsersController < ApplicationController
|
27
|
+
def show
|
28
|
+
return :text => component("users/detail", params[:id])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
### From ActionView
|
33
|
+
|
34
|
+
<%= component "users/detail", @user %>
|
35
|
+
|
36
|
+
## More Features
|
37
|
+
|
38
|
+
### Caching
|
39
|
+
|
40
|
+
Any component action may be cached using the fragment caching you've configured on ActionController::Base. The command to cache a component action must come after the definition of the action itself. This is because the caching method wraps the action, which makes the caching work even if you call the action directly.
|
41
|
+
|
42
|
+
Example:
|
43
|
+
|
44
|
+
class UsersComponent < Components::Base
|
45
|
+
def details(user_id)
|
46
|
+
@user = User.find(user_id)
|
47
|
+
render
|
48
|
+
end
|
49
|
+
cache :details, :expires_in => 15.minutes
|
50
|
+
end
|
51
|
+
|
52
|
+
This will cache the returns from UsersComponent#details using a cache key like "users/details/5", where 5 is the user_id. The cache will only be good for fifteen minutes. See Components::Caching for more information.
|
53
|
+
|
54
|
+
### Helpers
|
55
|
+
|
56
|
+
All of the standard helper functionality exists for components. You may define a method on your component controller and use :helper_method to make it available in your views, or you may use :helper to add entire modules of extra methods to your views.
|
57
|
+
|
58
|
+
Be careful importing existing helpers, though, as some of them may try and break encapsulation by reading from the session, the request, or the params. You may need to rewrite these helpers so they accept the necessary information as arguments.
|
59
|
+
|
60
|
+
### Inherited Views
|
61
|
+
|
62
|
+
Assume two components:
|
63
|
+
|
64
|
+
class ParentComponent < Components::Base
|
65
|
+
def one
|
66
|
+
render
|
67
|
+
end
|
68
|
+
|
69
|
+
def two
|
70
|
+
render
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class ChildComponent < ParentComponent
|
75
|
+
def one
|
76
|
+
render
|
77
|
+
end
|
78
|
+
|
79
|
+
def three
|
80
|
+
render "one"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
Both methods on the ChildComponent class would first try and render "/app/components/child/one.erb", and if that file did not exist, would render "/app/components/parent/one.erb".
|
85
|
+
|
86
|
+
### Standard Argument Options
|
87
|
+
|
88
|
+
You may find yourself constantly needing to pass a standard set of options to each component. If so, you can define a method on your controller that returns a hash of standard options that will be merged with the component arguments and passed to every component.
|
89
|
+
|
90
|
+
Suppose a given component:
|
91
|
+
|
92
|
+
class GroupsComponent < Components::Base
|
93
|
+
def details(group_id, options = {})
|
94
|
+
@user = options[:user]
|
95
|
+
@group = Group.find(group_id)
|
96
|
+
render
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
Then the following setup:
|
101
|
+
|
102
|
+
class GroupsController < ApplicationController
|
103
|
+
def show
|
104
|
+
render :text => component("groups/details", params[:id])
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
def standard_component_options
|
110
|
+
{:user => current_user}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
Would expand to:
|
115
|
+
|
116
|
+
component("groups/details", params[:id], :user => current_user)
|
117
|
+
|
118
|
+
## Components Philosophy
|
119
|
+
|
120
|
+
I wrote this components plugin after evaluating a couple of existing ones, reflecting a bit, and either stealing or composing the following principles. I welcome all debate on the subject.
|
121
|
+
|
122
|
+
### Components <em>should not</em> simply embed existing controller actions.
|
123
|
+
|
124
|
+
Re-using existing controller actions introduces intractable performance problems related to redundant controller filters and duplicate request-cached variables.
|
125
|
+
|
126
|
+
### Components <em>should not</em> have the concept of a "request" or "current user".
|
127
|
+
|
128
|
+
Everything should be provided as an argument to the component - it should not have direct access to the session, the params, or any other aspect of the request. This means that components will never intelligently respond_to :html, :js, :xml, etc.
|
129
|
+
|
130
|
+
### Components _should_ complement RESTful controller design.
|
131
|
+
|
132
|
+
The path of least resistance in Rails includes RESTful controller design to reduce code redundancy. Components should only be designed for use cases where RESTful controller design is either awkward or impossible. This compatibility will reduce the maintenance effort for components and help them grow with Rails itself.
|
133
|
+
|
134
|
+
## Troubleshooting
|
135
|
+
|
136
|
+
Q: I want to render partials from my app/views directory in a component view but it doesn't work
|
137
|
+
A: You have to add the view path of your normal views to the view paths of the components. One solution would be to overwrite the `view_paths` method in your component:
|
138
|
+
|
139
|
+
def self.view_paths
|
140
|
+
super + [Rails.root + 'app/views']
|
141
|
+
end
|
142
|
+
|
143
|
+
Q: I want to use a method from one of my helpers in app/helpers but I get a method missing error
|
144
|
+
A: Call `helper :all` in your component, just like you would in a controller
|
145
|
+
|
146
|
+
## Copyright
|
147
|
+
|
148
|
+
Copyright (c) 2008 Lance Ivy, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "components"
|
8
|
+
gem.summary = "lightweight components for rails"
|
9
|
+
gem.email = "alex@upstream-berlin.com"
|
10
|
+
gem.homepage = "http://github.com/langalex/components"
|
11
|
+
gem.authors = ["Lance Ivy", "Alexander Lang"]
|
12
|
+
|
13
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
14
|
+
end
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
Rake::TestTask.new(:test) do |test|
|
21
|
+
test.libs << 'lib' << 'test'
|
22
|
+
test.pattern = 'test/**/*_test.rb'
|
23
|
+
test.verbose = false
|
24
|
+
end
|
25
|
+
|
26
|
+
task :default => :test
|
27
|
+
|
28
|
+
require 'rake/rdoctask'
|
29
|
+
Rake::RDocTask.new do |rdoc|
|
30
|
+
if File.exist?('VERSION.yml')
|
31
|
+
config = YAML.load(File.read('VERSION.yml'))
|
32
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
33
|
+
else
|
34
|
+
version = ""
|
35
|
+
end
|
36
|
+
|
37
|
+
rdoc.rdoc_dir = 'rdoc'
|
38
|
+
rdoc.title = "components #{version}"
|
39
|
+
rdoc.rdoc_files.include('README*')
|
40
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
41
|
+
end
|
42
|
+
|
data/VERSION.yml
ADDED
data/components.gemspec
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{components}
|
8
|
+
s.version = "0.0.5"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Lance Ivy", "Alexander Lang"]
|
12
|
+
s.date = %q{2009-10-10}
|
13
|
+
s.email = %q{alex@upstream-berlin.com}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"README.md"
|
16
|
+
]
|
17
|
+
s.files = [
|
18
|
+
".document",
|
19
|
+
".gitignore",
|
20
|
+
"MIT-LICENSE",
|
21
|
+
"README.md",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION.yml",
|
24
|
+
"components.gemspec",
|
25
|
+
"generators/component/component_generator.rb",
|
26
|
+
"generators/component/templates/component_template.rb",
|
27
|
+
"generators/component/templates/view_template.rb",
|
28
|
+
"lib/components.rb",
|
29
|
+
"lib/components/base.rb",
|
30
|
+
"lib/components/caching.rb",
|
31
|
+
"lib/components/helpers.rb",
|
32
|
+
"lib/components/view.rb",
|
33
|
+
"rails/init.rb",
|
34
|
+
"test/action_controller_test.rb",
|
35
|
+
"test/app/components/child/one.erb",
|
36
|
+
"test/app/components/child_component.rb",
|
37
|
+
"test/app/components/hello_world/bolded.erb",
|
38
|
+
"test/app/components/hello_world/say_it_with_help.erb",
|
39
|
+
"test/app/components/hello_world_component.rb",
|
40
|
+
"test/app/components/parent/one.erb",
|
41
|
+
"test/app/components/parent/two.erb",
|
42
|
+
"test/app/components/parent_component.rb",
|
43
|
+
"test/app/components/rich_view/form.erb",
|
44
|
+
"test/app/components/rich_view/linker.erb",
|
45
|
+
"test/app/components/rich_view/urler.erb",
|
46
|
+
"test/app/components/rich_view_component.rb",
|
47
|
+
"test/app/controllers/application_controller.rb",
|
48
|
+
"test/app/controllers/hello_world_controller.rb",
|
49
|
+
"test/caching_test.rb",
|
50
|
+
"test/components_test.rb",
|
51
|
+
"test/test_helper.rb"
|
52
|
+
]
|
53
|
+
s.homepage = %q{http://github.com/langalex/components}
|
54
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
55
|
+
s.require_paths = ["lib"]
|
56
|
+
s.rubygems_version = %q{1.3.5}
|
57
|
+
s.summary = %q{lightweight components for rails}
|
58
|
+
s.test_files = [
|
59
|
+
"test/action_controller_test.rb",
|
60
|
+
"test/app/components/child_component.rb",
|
61
|
+
"test/app/components/hello_world_component.rb",
|
62
|
+
"test/app/components/parent_component.rb",
|
63
|
+
"test/app/components/rich_view_component.rb",
|
64
|
+
"test/app/controllers/application_controller.rb",
|
65
|
+
"test/app/controllers/hello_world_controller.rb",
|
66
|
+
"test/caching_test.rb",
|
67
|
+
"test/components_test.rb",
|
68
|
+
"test/test_helper.rb"
|
69
|
+
]
|
70
|
+
|
71
|
+
if s.respond_to? :specification_version then
|
72
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
73
|
+
s.specification_version = 3
|
74
|
+
|
75
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
76
|
+
else
|
77
|
+
end
|
78
|
+
else
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class ComponentGenerator < Rails::Generator::NamedBase
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
m.class_collisions "#{class_name}Component"
|
5
|
+
m.directory "app/components/#{file_name}"
|
6
|
+
m.template "component_template.rb", "app/components/#{file_name}_component.rb"
|
7
|
+
|
8
|
+
actions.each do |action|
|
9
|
+
m.template "view_template.rb", "app/components/#{file_name}/#{action}.erb"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
File without changes
|
data/lib/components.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module Components #:nodoc:
|
2
|
+
def self.render(name, component_args = [], options = {})
|
3
|
+
klass, method = name.split('/')
|
4
|
+
component = (klass + "_component").camelcase.constantize.new
|
5
|
+
component.form_authenticity_token = options[:form_authenticity_token]
|
6
|
+
component._request = options[:request]
|
7
|
+
merge_standard_component_options!(component_args, options[:standard_component_options], component.method(method).arity)
|
8
|
+
component.logger.debug "Rendering component #{name}"
|
9
|
+
component.send(method, *component_args).to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.merge_standard_component_options!(args, standard_options, arity)
|
13
|
+
if standard_options
|
14
|
+
# when the method's arity is positive, it only accepts a fixed list of arguments, so we can't add another.
|
15
|
+
args << {} unless args.last.is_a?(Hash) or not arity < 0
|
16
|
+
args.last.reverse_merge!(standard_options) if args.last.is_a?(Hash)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ActionController
|
21
|
+
protected
|
22
|
+
|
23
|
+
# Renders the named component with the given arguments. The component name must indicate both
|
24
|
+
# the component class and the class' action.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
#
|
28
|
+
# class UsersController < ApplicationController
|
29
|
+
# def show
|
30
|
+
# render :text => component("users/details", params[:id])
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# would render:
|
35
|
+
#
|
36
|
+
# class UsersComponent < Components::Base
|
37
|
+
# def details(user_id)
|
38
|
+
# "all the important details about the user, nicely marked up"
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
def component(name, *args)
|
42
|
+
Components.render(name, args,
|
43
|
+
:form_authenticity_token => (form_authenticity_token if protect_against_forgery?),
|
44
|
+
:standard_component_options => standard_component_options,
|
45
|
+
:request => request
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Override this method on your controller (probably your ApplicationController)
|
50
|
+
# to define common arguments for your components. For example, you may always
|
51
|
+
# want to provide the current user, in which case you could return {:user =>
|
52
|
+
# current_user} from this method.
|
53
|
+
#
|
54
|
+
# In order to use this, your component actions should accept an options hash
|
55
|
+
# as their last argument.
|
56
|
+
#
|
57
|
+
# I feel like this is a better solution than simply making request and
|
58
|
+
# session details directly available to the components.
|
59
|
+
def standard_component_options; end
|
60
|
+
end
|
61
|
+
|
62
|
+
module ActionView
|
63
|
+
# Renders the named component with the given arguments. The component name must indicate both
|
64
|
+
# the component class and the class' action.
|
65
|
+
#
|
66
|
+
# Example:
|
67
|
+
#
|
68
|
+
# /app/views/users/show.html.erb
|
69
|
+
#
|
70
|
+
# <%= component "users/details", @user.id %>
|
71
|
+
#
|
72
|
+
# would render:
|
73
|
+
#
|
74
|
+
# class UsersComponent < Components::Base
|
75
|
+
# def details(user_id)
|
76
|
+
# "all the important details about the user, nicely marked up"
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
def component(name, *args)
|
80
|
+
Components.render(name, args,
|
81
|
+
:form_authenticity_token => (form_authenticity_token if protect_against_forgery?),
|
82
|
+
:standard_component_options => controller.send(:standard_component_options),
|
83
|
+
:controller => controller,
|
84
|
+
:request => request
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
ActionController::Base.class_eval do include Components::ActionController end
|
91
|
+
ActionView::Base.class_eval do include Components::ActionView end
|
92
|
+
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Components
|
2
|
+
class Base
|
3
|
+
include ::ActionController::UrlWriter
|
4
|
+
include ::ActionController::Helpers
|
5
|
+
include ::Components::Caching
|
6
|
+
|
7
|
+
# for request forgery protection compatibility
|
8
|
+
attr_accessor :form_authenticity_token, :_request #:nodoc:
|
9
|
+
delegate :request_forgery_protection_token, :allow_forgery_protection, :to => "ActionController::Base"
|
10
|
+
|
11
|
+
def protect_against_forgery? #:nodoc:
|
12
|
+
allow_forgery_protection && request_forgery_protection_token
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# The view paths to search for templates. Typically this will only be "app/components", but
|
17
|
+
# if you have a plugin that uses Components, it may add its own directory (e.g.
|
18
|
+
# "vendor/plugins/scaffolding/components/" to this array.
|
19
|
+
def view_paths
|
20
|
+
if read_inheritable_attribute(:view_paths).nil?
|
21
|
+
default_path = File.join(RAILS_ROOT, 'app', 'components')
|
22
|
+
write_inheritable_attribute(:view_paths, [default_path])
|
23
|
+
end
|
24
|
+
read_inheritable_attribute(:view_paths)
|
25
|
+
end
|
26
|
+
|
27
|
+
def path #:nodoc:
|
28
|
+
@path ||= self.to_s.sub("Component", "").underscore
|
29
|
+
end
|
30
|
+
alias_method :controller_path, :path
|
31
|
+
|
32
|
+
attr_accessor :template
|
33
|
+
end
|
34
|
+
|
35
|
+
# must be public for access from ActionView
|
36
|
+
def logger #:nodoc:
|
37
|
+
RAILS_DEFAULT_LOGGER
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
# See Components::ActionController#standard_component_options
|
43
|
+
def standard_component_options; end
|
44
|
+
|
45
|
+
# When the string your component must return is complex enough to warrant a template file,
|
46
|
+
# this will render that file and return the result. Any template engine (erb, haml, etc.)
|
47
|
+
# that ActionView is capable of using can be used for templating.
|
48
|
+
#
|
49
|
+
# All instance variables that you create in the component action will be available from
|
50
|
+
# the view. There is currently no other way to provide variables to the views.
|
51
|
+
#
|
52
|
+
# === Inferred Template Name
|
53
|
+
#
|
54
|
+
# If you call render without a file name, it will:
|
55
|
+
# * assume that the name of the calling method is also the name of the template file
|
56
|
+
# * search for the named template file in the directory of this component's views, then the directories of all parent components
|
57
|
+
#
|
58
|
+
# This means that if you have:
|
59
|
+
#
|
60
|
+
# class UsersComponent < Components::Base
|
61
|
+
# def details(user_id)
|
62
|
+
# render
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# Then render will essentially assume that you meant to render "users/details", which may
|
67
|
+
# be found at "app/components/users/details.erb".
|
68
|
+
def render(file = nil)
|
69
|
+
# infer the render file basename from the caller method.
|
70
|
+
unless file
|
71
|
+
caller.first =~ /`([^']*)'/
|
72
|
+
file = $1.sub("_without_caching", '')
|
73
|
+
end
|
74
|
+
|
75
|
+
# pick the closest parent component with the file
|
76
|
+
component = self.class
|
77
|
+
result = nil
|
78
|
+
if file.include?("/")
|
79
|
+
result = render_template("#{component.path}/#{file}")
|
80
|
+
else
|
81
|
+
until result
|
82
|
+
if component.superclass == Components::Base
|
83
|
+
result = render_template("#{component.path}/#{file}")
|
84
|
+
else
|
85
|
+
result = render_template("#{component.path}/#{file}") rescue nil
|
86
|
+
end
|
87
|
+
component = component.superclass
|
88
|
+
end
|
89
|
+
end
|
90
|
+
result
|
91
|
+
end
|
92
|
+
|
93
|
+
# creates and returns a view object for rendering the current action.
|
94
|
+
# note that this freezes knowledge of view_paths
|
95
|
+
def template #:nodoc:
|
96
|
+
template = self.class.template
|
97
|
+
if template.nil?
|
98
|
+
view_paths = self.class.view_paths
|
99
|
+
template = Components::View.new(view_paths, assigns_for_view, self)
|
100
|
+
template.extend self.class.master_helper_module
|
101
|
+
end
|
102
|
+
self.class.template = template
|
103
|
+
end
|
104
|
+
|
105
|
+
# should return a hash of all instance variables to assign to the view
|
106
|
+
def assigns_for_view #:nodoc:
|
107
|
+
(instance_variables - unassignable_instance_variables).inject({}) do |hash, var|
|
108
|
+
hash[var[1..-1]] = instance_variable_get(var)
|
109
|
+
hash
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# should name all of the instance variables used by Components::Base that should _not_ be accessible from the view.
|
114
|
+
def unassignable_instance_variables #:nodoc:
|
115
|
+
%w(@template @assigns_for_view)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def render_template(name)
|
121
|
+
template.controller = self
|
122
|
+
template.send('_copy_ivars_from_controller')
|
123
|
+
template.render({:file => name}, assigns_for_view)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# Component caching is very fine-grained - a component is cached based on all
|
2
|
+
# of its arguments. Any more or less would result in inaccurate cache hits.
|
3
|
+
#
|
4
|
+
# === Howto
|
5
|
+
#
|
6
|
+
# First, configure fragment caching on ActionController. That cache store will be used
|
7
|
+
# for components as well.
|
8
|
+
#
|
9
|
+
# Second, decide which component actions should be cached. Some components cache better
|
10
|
+
# than others due to the nature of the arguments passed.
|
11
|
+
#
|
12
|
+
# For each component you wish to cache, add `cache :my_action` _after_ you define the
|
13
|
+
# action itself. This is necessary because your action will be wrapped with another
|
14
|
+
# method - :my_action_with_caching. This enables component actions to be cached no matter
|
15
|
+
# how they are called.
|
16
|
+
#
|
17
|
+
# === Example
|
18
|
+
#
|
19
|
+
# class UserComponent < Components::Base
|
20
|
+
# def show(user_id)
|
21
|
+
# @user = User.find(user_id)
|
22
|
+
# render
|
23
|
+
# end
|
24
|
+
# cache :show
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# === Expiration
|
28
|
+
#
|
29
|
+
# I know of three general methods to expire caches:
|
30
|
+
#
|
31
|
+
# * TTL: expires a cache after some number of seconds. This works well for content that
|
32
|
+
# is hit frequently but can stand to be a bit stale at times.
|
33
|
+
# * Versioning: caches are never actually expired, rather, they are eventually ignored
|
34
|
+
# when all new cache requests are for a new version. This only works with cache stores
|
35
|
+
# that have some kind of limited cache space, otherwise cache consumption will go
|
36
|
+
# through the metaphorical roof.
|
37
|
+
# * Direct Expiration: caches are expired by name. This does not work when cache keys
|
38
|
+
# have variable elements, or when the complete list of cache keys is not available
|
39
|
+
# or a brute-force regular expression approach.
|
40
|
+
#
|
41
|
+
# Of those three, direct expiration is not a viable option due to the variable nature of
|
42
|
+
# component cache keys. And since both of the remaining methods are best supported by some
|
43
|
+
# variation of memcache, that is the officially recommended cache store.
|
44
|
+
#
|
45
|
+
# ==== TTL Expiration
|
46
|
+
#
|
47
|
+
# If you are using Rails' :mem_cache_store for fragments, then you can set up TTL-style
|
48
|
+
# expiration by specifying an :expires_in option, like so:
|
49
|
+
#
|
50
|
+
# class UserComponent < Components::Base
|
51
|
+
# def show(user_id)
|
52
|
+
# @user = User.find(user_id)
|
53
|
+
# render
|
54
|
+
# end
|
55
|
+
# cache :show, :expires_in => 15.minutes
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# ==== Versioned Expiration
|
59
|
+
#
|
60
|
+
# Maintaining and incrementing version numbers may be implemented any number of ways. To
|
61
|
+
# use the version numbers, though, you can specify a :version option, which may either name
|
62
|
+
# a method (use a Symbol) or provide a proc. In either case, the method or proc should
|
63
|
+
# receive all of the same arguments as the action itself, and should return the version
|
64
|
+
# string.
|
65
|
+
#
|
66
|
+
# class UserComponent < Components::Base
|
67
|
+
# def show(user_id)
|
68
|
+
# @user = User.find(user_id)
|
69
|
+
# render
|
70
|
+
# end
|
71
|
+
# cache :show, :version => :show_cache_version
|
72
|
+
#
|
73
|
+
# protected
|
74
|
+
#
|
75
|
+
# def show_cache_version(user_id)
|
76
|
+
# # you may want to find your version from a model object, from memcache, or whereever.
|
77
|
+
# Version.for("users/show", user_id)
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
module Components::Caching
|
82
|
+
def self.included(base) #:nodoc:
|
83
|
+
base.class_eval do
|
84
|
+
extend ClassMethods
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
module ClassMethods
|
89
|
+
# Caches the named actions by wrapping them via alias_method_chain. May only
|
90
|
+
# be called on actions (methods) that have already been defined.
|
91
|
+
#
|
92
|
+
# Cache options will be passed through to the cache store's read/write methods.
|
93
|
+
def cache(action, cache_options = nil)
|
94
|
+
return unless ActionController::Base.cache_configured?
|
95
|
+
|
96
|
+
class_eval <<-EOL, __FILE__, __LINE__
|
97
|
+
cattr_accessor :#{action}_cache_options
|
98
|
+
|
99
|
+
def #{action}_with_caching(*args)
|
100
|
+
with_caching(:#{action}, args) do
|
101
|
+
#{action}_without_caching(*args)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
alias_method_chain :#{action}, :caching
|
105
|
+
EOL
|
106
|
+
self.send("#{action}_cache_options=", cache_options)
|
107
|
+
end
|
108
|
+
|
109
|
+
def cache_store #:nodoc:
|
110
|
+
@cache_store ||= ActionController::Base.cache_store
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
def with_caching(action, args, &block) #:nodoc:
|
117
|
+
key = cache_key(action, args)
|
118
|
+
cache_options = self.send("#{action}_cache_options") || {}
|
119
|
+
passthrough_cache_options = cache_options.reject{|k, v| reserved_cache_option_keys.include? k}
|
120
|
+
passthrough_cache_options = nil if cache_options.empty?
|
121
|
+
|
122
|
+
# conditional caching: the prohibited case
|
123
|
+
if cache_options[:if] and not call(cache_options[:if], args)
|
124
|
+
fragment = block.call
|
125
|
+
else
|
126
|
+
fragment = read_fragment(key, passthrough_cache_options)
|
127
|
+
unless fragment
|
128
|
+
fragment = block.call
|
129
|
+
write_fragment(key, fragment, passthrough_cache_options)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
return fragment
|
134
|
+
end
|
135
|
+
|
136
|
+
def read_fragment(key, cache_options = nil) #:nodoc:
|
137
|
+
returning self.class.cache_store.read(key, cache_options) do |content|
|
138
|
+
logger.debug "Component Cache hit: #{key}" unless content.blank?
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def write_fragment(key, content, cache_options = nil) #:nodoc:
|
143
|
+
logger.debug "Component Cache miss: #{key}"
|
144
|
+
self.class.cache_store.write(key, content, cache_options)
|
145
|
+
end
|
146
|
+
|
147
|
+
# generates the cache key for the given action/args
|
148
|
+
def cache_key(action, args = []) #:nodoc:
|
149
|
+
key_pieces = [self.class.path, action] + args
|
150
|
+
|
151
|
+
if v = call(versioning(action), args)
|
152
|
+
key_pieces << "v#{v}"
|
153
|
+
end
|
154
|
+
key = key_pieces.collect do |arg|
|
155
|
+
case arg
|
156
|
+
when ActiveRecord::Base
|
157
|
+
"#{arg.class.to_s.underscore}#{arg.id}" # note: doesn't apply to record sets
|
158
|
+
else
|
159
|
+
arg.to_param
|
160
|
+
end
|
161
|
+
end.join('/').gsub(' ', '_')
|
162
|
+
|
163
|
+
key = Digest::MD5.hexdigest(key) if key.length > 200
|
164
|
+
|
165
|
+
ActiveSupport::Cache.expand_cache_key(key, :components)
|
166
|
+
end
|
167
|
+
|
168
|
+
# returns the versioning configuration for the given action, if any
|
169
|
+
def versioning(action) #:nodoc:
|
170
|
+
(self.send("#{action}_cache_options") || {})[:version]
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def call(method, args)
|
176
|
+
case method
|
177
|
+
when Proc
|
178
|
+
method.call(*args)
|
179
|
+
when Symbol
|
180
|
+
send(method, *args)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def reserved_cache_option_keys
|
185
|
+
@reserved_cache_option_keys ||= [:if, :version]
|
186
|
+
end
|
187
|
+
end
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Components::View < ActionView::Base
|
2
|
+
delegate :protect_against_forgery?, :form_authenticity_token, :to => :controller
|
3
|
+
|
4
|
+
# TODO: rendering from a component view should be restricted to *just* rendering other components.
|
5
|
+
#
|
6
|
+
|
7
|
+
attr_accessor :controller
|
8
|
+
|
9
|
+
def request
|
10
|
+
controller._request
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class ActionControllerTest < ActionController::TestCase
|
4
|
+
::ActionController::Routing::Routes.draw do |map|
|
5
|
+
map.connect "/:controller/:action/:id"
|
6
|
+
end
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@controller = HelloWorldController.new
|
10
|
+
@request = ActionController::TestRequest.new
|
11
|
+
@response = ActionController::TestResponse.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_rendering_component_from_controller
|
15
|
+
get :say_it, :string => "onomatopoeia"
|
16
|
+
assert_equal "onomatopoeia", @response.body
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_standard_component_options
|
20
|
+
HelloWorldController.any_instance.stubs(:standard_component_options).returns(:user => "snuffalumpagus")
|
21
|
+
HelloWorldComponent.any_instance.expects(:say_it).with("wawoowifnik", :user => "snuffalumpagus")
|
22
|
+
get :say_it, :string => "wawoowifnik"
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
child/one
|
@@ -0,0 +1 @@
|
|
1
|
+
<b><%= @string %></b>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= get_string %>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class HelloWorldComponent < Components::Base
|
2
|
+
helper_method :get_string
|
3
|
+
|
4
|
+
def say_it(string)
|
5
|
+
string
|
6
|
+
end
|
7
|
+
|
8
|
+
def say_it_with_style(string)
|
9
|
+
bolded(string)
|
10
|
+
end
|
11
|
+
|
12
|
+
def say_it_with_help(string)
|
13
|
+
@string = string
|
14
|
+
render
|
15
|
+
end
|
16
|
+
|
17
|
+
def bolded(string)
|
18
|
+
@string = string
|
19
|
+
render
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def get_string
|
25
|
+
@string
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
parent/one
|
@@ -0,0 +1 @@
|
|
1
|
+
parent/two
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= link_to "linking is essential", @url %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= url_for(:controller => "foo", :action => "bar") %>
|
File without changes
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class CachingTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
ActionController::Base.stubs(:cache_configured?).returns(true)
|
6
|
+
klass = HelloWorldComponent.dup
|
7
|
+
klass.stubs(:path).returns("hello_world")
|
8
|
+
klass.send(:cache, :say_it)
|
9
|
+
@component = klass.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_cache_method_chaining
|
13
|
+
@component.expects(:with_caching).with(:say_it, ["janadumplet"]).returns("janadoomplet")
|
14
|
+
assert_equal "janadoomplet", @component.say_it("janadumplet")
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_cache_key_generation
|
18
|
+
assert_equal "components/hello_world/say_it", @component.send(:cache_key, :say_it), "simplest cache key"
|
19
|
+
assert_equal "components/hello_world/say_it/trumpapum", @component.send(:cache_key, :say_it, ["trumpapum"]), "uses arguments"
|
20
|
+
assert_equal "components/hello_world/say_it/a/1/2/3/foo=bar", @component.send(:cache_key, :say_it, ["a", [1,2,3], {:foo => :bar}]), "handles mixed types"
|
21
|
+
assert_equal "components/hello_world/say_it/a=1&b=2", @component.send(:cache_key, :say_it, [{:b => 2, :a => 1}]), "hash keys are ordered"
|
22
|
+
assert_equal "components/834876df77918cf2bbfb42253d5977aa", @component.send(:cache_key, :say_it, [{:a => 'x' * 190}]), "hash keys are MD5ed when too long"
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_conditional_caching
|
26
|
+
@component.say_it_cache_options = {:if => proc{false}}
|
27
|
+
@component.expects(:read_fragment).never
|
28
|
+
assert_equal "trimpanta", @component.say_it("trimpanta")
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_cache_hit
|
32
|
+
@component.expects(:read_fragment).with("components/hello_world/say_it/loudly", nil).returns("LOUDLY!")
|
33
|
+
@component.expects(:say_it_without_caching).never
|
34
|
+
@component.say_it("loudly")
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_cache_miss
|
38
|
+
@component.expects(:read_fragment).returns(nil)
|
39
|
+
@component.expects(:write_fragment).with("components/hello_world/say_it/frumpamumpa", "frumpamumpa", nil)
|
40
|
+
assert_equal "frumpamumpa", @component.say_it("frumpamumpa")
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_expires_in_passthrough
|
44
|
+
@component.say_it_cache_options = {:expires_in => 15.minutes}
|
45
|
+
@component.expects(:write_fragment).with("components/hello_world/say_it/ninnanana", "ninnanana", {:expires_in => 15.minutes})
|
46
|
+
assert_equal "ninnanana", @component.say_it("ninnanana")
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_versioned_keys
|
50
|
+
@component.say_it_cache_options = {:version => :some_named_method}
|
51
|
+
@component.expects(:some_named_method).with("rangleratta").returns(314)
|
52
|
+
assert_equal "components/hello_world/say_it/rangleratta/v314", @component.send(:cache_key, :say_it, ["rangleratta"])
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_versioned_keys_dont_have_spaces
|
56
|
+
@component.say_it_cache_options = {:version => :some_named_method}
|
57
|
+
@component.expects(:some_named_method).with("rangleratta").returns('2009 10 31')
|
58
|
+
assert_equal "components/hello_world/say_it/rangleratta/v2009_10_31", @component.send(:cache_key, :say_it, ["rangleratta"])
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class ComponentsTest < ActionController::TestCase
|
4
|
+
def test_component_response
|
5
|
+
HelloWorldComponent.any_instance.expects(:say_it).returns("mukadoogle")
|
6
|
+
assert_equal "mukadoogle", Components.render("hello_world/say_it", ["gigglemuppit"])
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_rendering_a_component_view
|
10
|
+
assert_equal "<b>pifferspangle</b>", Components.render("hello_world/say_it_with_style", ["pifferspangle"])
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_implied_render_file
|
14
|
+
assert_equal "<b>foofididdums</b>", Components.render("hello_world/bolded", ["foofididdums"])
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_inherited_views
|
18
|
+
assert_equal "parent/one", Components.render("parent/one")
|
19
|
+
assert_equal "parent/two", Components.render("parent/two")
|
20
|
+
assert_equal "child/one", Components.render("child/one")
|
21
|
+
assert_equal "parent/two", Components.render("child/two")
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_links_in_views
|
25
|
+
rendered = Components.render("rich_view/linker", ["http://example.com"])
|
26
|
+
assert_select rendered, "a[href=http://example.com]"
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_form_in_views
|
30
|
+
ActionController::Base.request_forgery_protection_token = :authenticity_token
|
31
|
+
rendered = Components.render("rich_view/form", [], :form_authenticity_token => "bluetlecrashit")
|
32
|
+
assert_select rendered, "form"
|
33
|
+
assert_select rendered, "input[type=hidden][name=authenticity_token][value=bluetlecrashit]"
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_url_for_in_views
|
37
|
+
assert_nothing_raised do
|
38
|
+
ActionController::Routing::RouteSet.any_instance.stubs(:generate).returns("some_url")
|
39
|
+
Components.render("rich_view/urler")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_helper_methods
|
44
|
+
assert_equal "jingleheimer", Components.render("hello_world/say_it_with_help", ["jingleheimer"])
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
def assert_select(content, *args)
|
50
|
+
super(HTML::Document.new(content).root, *args)
|
51
|
+
end
|
52
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# fake the rails root
|
2
|
+
RAILS_ROOT = File.dirname(__FILE__)
|
3
|
+
|
4
|
+
# require support libraries
|
5
|
+
require 'test/unit'
|
6
|
+
require 'rubygems'
|
7
|
+
gem 'rails', '2.3.2'
|
8
|
+
require 'active_support'
|
9
|
+
require 'action_controller'
|
10
|
+
require 'action_controller/test_process' # for the assertions
|
11
|
+
require 'action_view'
|
12
|
+
require 'active_record'
|
13
|
+
require 'logger'
|
14
|
+
require 'mocha'
|
15
|
+
|
16
|
+
RAILS_DEFAULT_LOGGER = Logger.new(File.dirname(__FILE__) + '/debug.log')
|
17
|
+
|
18
|
+
%w(../lib app/controllers).each do |load_path|
|
19
|
+
ActiveSupport::Dependencies.load_paths << File.dirname(__FILE__) + "/" + load_path
|
20
|
+
end
|
21
|
+
|
22
|
+
require File.dirname(__FILE__) + '/../rails/init'
|
23
|
+
|
24
|
+
ActionController::Base.cache_store = :memory_store
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: components
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lance Ivy
|
8
|
+
- Alexander Lang
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2009-10-10 00:00:00 +02:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description:
|
18
|
+
email: alex@upstream-berlin.com
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files:
|
24
|
+
- README.md
|
25
|
+
files:
|
26
|
+
- .document
|
27
|
+
- .gitignore
|
28
|
+
- MIT-LICENSE
|
29
|
+
- README.md
|
30
|
+
- Rakefile
|
31
|
+
- VERSION.yml
|
32
|
+
- components.gemspec
|
33
|
+
- generators/component/component_generator.rb
|
34
|
+
- generators/component/templates/component_template.rb
|
35
|
+
- generators/component/templates/view_template.rb
|
36
|
+
- lib/components.rb
|
37
|
+
- lib/components/base.rb
|
38
|
+
- lib/components/caching.rb
|
39
|
+
- lib/components/helpers.rb
|
40
|
+
- lib/components/view.rb
|
41
|
+
- rails/init.rb
|
42
|
+
- test/action_controller_test.rb
|
43
|
+
- test/app/components/child/one.erb
|
44
|
+
- test/app/components/child_component.rb
|
45
|
+
- test/app/components/hello_world/bolded.erb
|
46
|
+
- test/app/components/hello_world/say_it_with_help.erb
|
47
|
+
- test/app/components/hello_world_component.rb
|
48
|
+
- test/app/components/parent/one.erb
|
49
|
+
- test/app/components/parent/two.erb
|
50
|
+
- test/app/components/parent_component.rb
|
51
|
+
- test/app/components/rich_view/form.erb
|
52
|
+
- test/app/components/rich_view/linker.erb
|
53
|
+
- test/app/components/rich_view/urler.erb
|
54
|
+
- test/app/components/rich_view_component.rb
|
55
|
+
- test/app/controllers/application_controller.rb
|
56
|
+
- test/app/controllers/hello_world_controller.rb
|
57
|
+
- test/caching_test.rb
|
58
|
+
- test/components_test.rb
|
59
|
+
- test/test_helper.rb
|
60
|
+
has_rdoc: true
|
61
|
+
homepage: http://github.com/langalex/components
|
62
|
+
licenses: []
|
63
|
+
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options:
|
66
|
+
- --charset=UTF-8
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: "0"
|
74
|
+
version:
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: "0"
|
80
|
+
version:
|
81
|
+
requirements: []
|
82
|
+
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.3.5
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: lightweight components for rails
|
88
|
+
test_files:
|
89
|
+
- test/action_controller_test.rb
|
90
|
+
- test/app/components/child_component.rb
|
91
|
+
- test/app/components/hello_world_component.rb
|
92
|
+
- test/app/components/parent_component.rb
|
93
|
+
- test/app/components/rich_view_component.rb
|
94
|
+
- test/app/controllers/application_controller.rb
|
95
|
+
- test/app/controllers/hello_world_controller.rb
|
96
|
+
- test/caching_test.rb
|
97
|
+
- test/components_test.rb
|
98
|
+
- test/test_helper.rb
|