action_component 0.1.0 → 0.1.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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +83 -0
- data/README.md +24 -2
- data/action_component.gemspec +1 -0
- data/lib/action_component.rb +5 -1
- data/lib/action_component/base.rb +12 -61
- data/lib/action_component/constraints.rb +52 -0
- data/lib/action_component/elements.rb +74 -0
- data/lib/action_component/railtie.rb +2 -0
- data/lib/action_component/version.rb +1 -1
- data/spec/action_component/base_spec.rb +85 -0
- data/spec/action_component/constraints_spec.rb +34 -0
- data/spec/action_component/elements_spec.rb +116 -0
- data/spec/examples.txt +21 -0
- data/spec/fake_view.rb +20 -0
- data/spec/spec_helper.rb +102 -0
- metadata +34 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 255003a7f08bfb6d27d4994f5bcbd598ed1c12ad
|
4
|
+
data.tar.gz: 096cf23179c4f7c49f6168d484b6af4f4396ca97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c676138c609aec84ff3b027377d086e1e1d9d60b8b7a88f82a614927eed75878d5321c0269a2391dc90cda53bb1a85576c707016ac725e40849add50b2e002b
|
7
|
+
data.tar.gz: 9c3a1f61257e06e780412d2d319c83a55bb82285e2e70ddf59522a3c50058592fefbcf0eccc10b3ab74dc538de2aa53af6f97d71758cea7af574352038ada8d6
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
action_component (0.1.1)
|
5
|
+
actionpack (>= 4)
|
6
|
+
activesupport (>= 4)
|
7
|
+
railties (>= 4)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
actionpack (5.0.0.1)
|
13
|
+
actionview (= 5.0.0.1)
|
14
|
+
activesupport (= 5.0.0.1)
|
15
|
+
rack (~> 2.0)
|
16
|
+
rack-test (~> 0.6.3)
|
17
|
+
rails-dom-testing (~> 2.0)
|
18
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
19
|
+
actionview (5.0.0.1)
|
20
|
+
activesupport (= 5.0.0.1)
|
21
|
+
builder (~> 3.1)
|
22
|
+
erubis (~> 2.7.0)
|
23
|
+
rails-dom-testing (~> 2.0)
|
24
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
25
|
+
activesupport (5.0.0.1)
|
26
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
27
|
+
i18n (~> 0.7)
|
28
|
+
minitest (~> 5.1)
|
29
|
+
tzinfo (~> 1.1)
|
30
|
+
builder (3.2.2)
|
31
|
+
concurrent-ruby (1.0.2)
|
32
|
+
diff-lcs (1.2.5)
|
33
|
+
erubis (2.7.0)
|
34
|
+
i18n (0.7.0)
|
35
|
+
loofah (2.0.3)
|
36
|
+
nokogiri (>= 1.5.9)
|
37
|
+
method_source (0.8.2)
|
38
|
+
mini_portile2 (2.1.0)
|
39
|
+
minitest (5.9.1)
|
40
|
+
nokogiri (1.6.8.1)
|
41
|
+
mini_portile2 (~> 2.1.0)
|
42
|
+
rack (2.0.1)
|
43
|
+
rack-test (0.6.3)
|
44
|
+
rack (>= 1.0)
|
45
|
+
rails-dom-testing (2.0.1)
|
46
|
+
activesupport (>= 4.2.0, < 6.0)
|
47
|
+
nokogiri (~> 1.6.0)
|
48
|
+
rails-html-sanitizer (1.0.3)
|
49
|
+
loofah (~> 2.0)
|
50
|
+
railties (5.0.0.1)
|
51
|
+
actionpack (= 5.0.0.1)
|
52
|
+
activesupport (= 5.0.0.1)
|
53
|
+
method_source
|
54
|
+
rake (>= 0.8.7)
|
55
|
+
thor (>= 0.18.1, < 2.0)
|
56
|
+
rake (11.3.0)
|
57
|
+
rspec (3.5.0)
|
58
|
+
rspec-core (~> 3.5.0)
|
59
|
+
rspec-expectations (~> 3.5.0)
|
60
|
+
rspec-mocks (~> 3.5.0)
|
61
|
+
rspec-core (3.5.4)
|
62
|
+
rspec-support (~> 3.5.0)
|
63
|
+
rspec-expectations (3.5.0)
|
64
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
65
|
+
rspec-support (~> 3.5.0)
|
66
|
+
rspec-mocks (3.5.0)
|
67
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
68
|
+
rspec-support (~> 3.5.0)
|
69
|
+
rspec-support (3.5.0)
|
70
|
+
thor (0.19.3)
|
71
|
+
thread_safe (0.3.5)
|
72
|
+
tzinfo (1.2.2)
|
73
|
+
thread_safe (~> 0.1)
|
74
|
+
|
75
|
+
PLATFORMS
|
76
|
+
ruby
|
77
|
+
|
78
|
+
DEPENDENCIES
|
79
|
+
action_component!
|
80
|
+
rspec
|
81
|
+
|
82
|
+
BUNDLED WITH
|
83
|
+
1.12.5
|
data/README.md
CHANGED
@@ -2,7 +2,20 @@
|
|
2
2
|
|
3
3
|
A React-style component system for Ruby on Rails.
|
4
4
|
|
5
|
-
|
5
|
+
As your application gets bigger, you'll find you have components which are common to multiple pages.
|
6
|
+
The normal Rails way is to use a `render :partial`, but you might have some logic and/or database
|
7
|
+
setup you want to do for this partial. Short of putting them in every controller that uses the
|
8
|
+
component, or even worse, putting them in the view, there's no elegant solution.
|
9
|
+
|
10
|
+
Enter ActionComponent. Encapsulate a component's setup logic and view in the same class, and
|
11
|
+
render the component either from an existing Rails view, or straight from the controller.
|
12
|
+
|
13
|
+
While you can use Rails views to render your component's HTML, you can also use the JSX-like language
|
14
|
+
directly in your component's Ruby code.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add `gem "action_component"` to your Gemfile and run `bundle`. Done.
|
6
19
|
|
7
20
|
## Examples
|
8
21
|
|
@@ -65,9 +78,13 @@ end
|
|
65
78
|
# app/components/post_component.rb
|
66
79
|
|
67
80
|
class PostComponent < ActionComponent::Base
|
81
|
+
# You can specify which variables must be passed to this component and their
|
82
|
+
# types (but only if you want to; by default it accepts everything.)
|
83
|
+
required post: Post
|
84
|
+
|
68
85
|
def view
|
69
86
|
div(class: 'post') do
|
70
|
-
h2
|
87
|
+
h2 @post.title
|
71
88
|
|
72
89
|
component AuthorComponent, author: @post.author
|
73
90
|
|
@@ -77,6 +94,11 @@ class PostComponent < ActionComponent::Base
|
|
77
94
|
end
|
78
95
|
```
|
79
96
|
|
97
|
+
## More documentation to come
|
98
|
+
|
99
|
+
ActionComponent is new. It works just fine, but at the moment if you need more information than is given above,
|
100
|
+
please dive into the (very small) codebase to learn more.
|
101
|
+
|
80
102
|
## Contributing
|
81
103
|
|
82
104
|
Pull requests welcome! If you're thinking of contributing a new feature, or
|
data/action_component.gemspec
CHANGED
data/lib/action_component.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
module ActionComponent
|
2
2
|
end
|
3
3
|
|
4
|
+
require 'action_component/version'
|
5
|
+
|
4
6
|
require 'action_component/action_controller_rendering'
|
5
7
|
require 'action_component/action_view_rendering'
|
8
|
+
require 'action_component/constraints'
|
9
|
+
require 'action_component/elements'
|
10
|
+
|
6
11
|
require 'action_component/base'
|
7
12
|
require 'action_component/railtie'
|
8
|
-
require 'action_component/version'
|
@@ -1,41 +1,26 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
1
3
|
module ActionComponent
|
2
4
|
RenderError = Class.new(StandardError)
|
3
|
-
|
4
|
-
|
5
|
-
html head title base link meta style
|
6
|
-
script noscript
|
7
|
-
body section nav article aside h1 h2 h3 h4 h5 h6 hgroup header footer address
|
8
|
-
p hr br pre blockquote ol ul li dl dt dd figure figcaption div
|
9
|
-
a em strong small s cite q dfn abbr time code var samp kbd sub sup i b u mark rt rp bdi bdo span
|
10
|
-
ins del
|
11
|
-
img iframe embed object param video audio source track canvas map area
|
12
|
-
table caption colgroup col tbody thead tfoot tr td th
|
13
|
-
form fieldset legend label input button select datalist optgroup option textarea keygen output progress meter
|
14
|
-
details summary command menu
|
15
|
-
)
|
5
|
+
ConstraintError = Class.new(StandardError)
|
6
|
+
ViewMissingError = Class.new(StandardError)
|
16
7
|
|
17
8
|
class Base
|
18
|
-
|
19
|
-
|
20
|
-
def self.define_tags(*element_names)
|
21
|
-
element_names.each do |element_name|
|
22
|
-
define_method(element_name) do |*args, &block|
|
23
|
-
element(element_name, *args, &block)
|
24
|
-
end
|
25
|
-
|
26
|
-
private element_name
|
27
|
-
end
|
28
|
-
end
|
9
|
+
include ActionComponent::Constraints
|
10
|
+
include ActionComponent::Elements
|
29
11
|
|
30
|
-
|
12
|
+
delegate :concat, to: :@_view
|
31
13
|
|
32
14
|
def self.render(view, opts = {})
|
33
15
|
component = new(view, opts)
|
34
16
|
component.load
|
35
17
|
component.view
|
18
|
+
nil
|
36
19
|
end
|
37
20
|
|
38
21
|
def initialize(view, opts = {})
|
22
|
+
check_constraints!(opts)
|
23
|
+
|
39
24
|
opts.each do |key, value|
|
40
25
|
instance_variable_set("@#{key}", value)
|
41
26
|
end
|
@@ -47,7 +32,7 @@ module ActionComponent
|
|
47
32
|
end
|
48
33
|
|
49
34
|
def view
|
50
|
-
raise "#{self.class.name} must define a view method to be a valid component"
|
35
|
+
raise ActionComponent::ViewMissingError, "#{self.class.name} must define a view method to be a valid component"
|
51
36
|
end
|
52
37
|
|
53
38
|
private
|
@@ -65,43 +50,9 @@ module ActionComponent
|
|
65
50
|
end
|
66
51
|
|
67
52
|
def component(component, opts = {})
|
68
|
-
|
53
|
+
component.render(@_view, opts)
|
69
54
|
end
|
70
55
|
|
71
56
|
alias_method :render_component, :component
|
72
|
-
|
73
|
-
def text(content)
|
74
|
-
@_view.concat content
|
75
|
-
end
|
76
|
-
|
77
|
-
alias_method :text_node, :text
|
78
|
-
alias_method :insert, :text
|
79
|
-
|
80
|
-
def doctype(text = "html")
|
81
|
-
@_view.concat("<!doctype #{h(text)}>".html_safe)
|
82
|
-
end
|
83
|
-
|
84
|
-
def element(name, first = nil, second = nil, &block)
|
85
|
-
if first.is_a?(Hash)
|
86
|
-
opts = first
|
87
|
-
else
|
88
|
-
opts = second
|
89
|
-
text = first
|
90
|
-
end
|
91
|
-
|
92
|
-
output = if text && block
|
93
|
-
raise ActionComponent::RenderError, "An element cannot have both text and a block supplied; choose one or the other"
|
94
|
-
elsif text
|
95
|
-
@_view.content_tag name, text, opts
|
96
|
-
elsif block
|
97
|
-
@_view.content_tag name, opts, &block
|
98
|
-
else
|
99
|
-
@_view.tag name, opts
|
100
|
-
end
|
101
|
-
|
102
|
-
@_view.concat(output)
|
103
|
-
|
104
|
-
nil
|
105
|
-
end
|
106
57
|
end
|
107
58
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/class/attribute'
|
3
|
+
|
4
|
+
module ActionComponent
|
5
|
+
module Constraints
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
Constraint = Struct.new(:name, :class_constraint, :required?)
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :initializer_constraints
|
12
|
+
|
13
|
+
self.initializer_constraints = []
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
def required(*fields)
|
18
|
+
fields.each do |field|
|
19
|
+
if field.is_a?(Hash)
|
20
|
+
field.each do |name, class_constraint|
|
21
|
+
self.initializer_constraints += [Constraint.new(name, class_constraint, true)]
|
22
|
+
end
|
23
|
+
else
|
24
|
+
self.initializer_constraints += [Constraint.new(field, nil, true)]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def optional(fields)
|
30
|
+
raise ActionComponent::ConstraintError, "optional can only take a hash of field names and classes" unless fields.is_a?(Hash)
|
31
|
+
|
32
|
+
fields.each do |name, class_constraint|
|
33
|
+
self.initializer_constraints += [Constraint.new(name, class_constraint, false)]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def check_constraints!(opts)
|
41
|
+
initializer_constraints.each do |constraint|
|
42
|
+
if constraint.required? && !opts.member?(constraint.name)
|
43
|
+
raise ActionComponent::ConstraintError, "#{constraint.name} is required for component #{self.class.name}"
|
44
|
+
end
|
45
|
+
|
46
|
+
if constraint.class_constraint && opts.member?(constraint.name) && !opts[constraint.name].is_a?(constraint.class_constraint)
|
47
|
+
raise ActionComponent::ConstraintError, "#{constraint.name} must be a #{constraint.class_constraint.name}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/string/output_safety'
|
3
|
+
|
4
|
+
module ActionComponent
|
5
|
+
module Elements
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ERB::Util
|
8
|
+
|
9
|
+
ELEMENTS = %w(
|
10
|
+
html head title base link meta style
|
11
|
+
script noscript
|
12
|
+
body section nav article aside h1 h2 h3 h4 h5 h6 hgroup header footer address
|
13
|
+
p hr br pre blockquote ol ul li dl dt dd figure figcaption div
|
14
|
+
a em strong small s cite q dfn abbr time code var samp kbd sub sup i b u mark rt rp bdi bdo span
|
15
|
+
ins del
|
16
|
+
img iframe embed object param video audio source track canvas map area
|
17
|
+
table caption colgroup col tbody thead tfoot tr td th
|
18
|
+
form fieldset legend label input button select datalist optgroup option textarea keygen output progress meter
|
19
|
+
details summary command menu
|
20
|
+
)
|
21
|
+
|
22
|
+
included do
|
23
|
+
define_tags *ELEMENTS
|
24
|
+
|
25
|
+
alias_method :text_node, :text
|
26
|
+
alias_method :insert, :text
|
27
|
+
end
|
28
|
+
|
29
|
+
class_methods do
|
30
|
+
def define_tags(*element_names)
|
31
|
+
element_names.each do |element_name|
|
32
|
+
define_method(element_name) do |*args, &block|
|
33
|
+
element(element_name, *args, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
private element_name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def text(content)
|
44
|
+
@_view.concat content
|
45
|
+
end
|
46
|
+
|
47
|
+
def doctype(text = "html")
|
48
|
+
@_view.concat("<!doctype #{h(text)}>".html_safe)
|
49
|
+
end
|
50
|
+
|
51
|
+
def element(name, first = nil, second = nil, &block)
|
52
|
+
if first.is_a?(Hash)
|
53
|
+
opts = first
|
54
|
+
else
|
55
|
+
opts = second
|
56
|
+
text = first
|
57
|
+
end
|
58
|
+
|
59
|
+
output = if text && block
|
60
|
+
raise ActionComponent::RenderError, "An element cannot have both text and a block supplied; choose one or the other"
|
61
|
+
elsif text
|
62
|
+
@_view.content_tag name, text, opts
|
63
|
+
elsif block
|
64
|
+
@_view.content_tag name, opts, &block
|
65
|
+
else
|
66
|
+
@_view.tag name, opts
|
67
|
+
end
|
68
|
+
|
69
|
+
@_view.concat(output)
|
70
|
+
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActionComponent::Base do
|
4
|
+
let(:view) { double }
|
5
|
+
let(:opts) { {option: 'value'} }
|
6
|
+
|
7
|
+
subject { ActionComponent::Base.new(view, opts) }
|
8
|
+
|
9
|
+
describe "::render" do
|
10
|
+
it "makes a new component, then calls load and view, returning nil" do
|
11
|
+
component = instance_double(ActionComponent::Base)
|
12
|
+
expect(component).to receive(:load).ordered
|
13
|
+
expect(component).to receive(:view).ordered
|
14
|
+
|
15
|
+
expect(ActionComponent::Base).to receive(:new)
|
16
|
+
.with(view, {option: 'value'})
|
17
|
+
.and_return(component)
|
18
|
+
|
19
|
+
result = ActionComponent::Base.render(view, option: 'value')
|
20
|
+
expect(result).to be nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#load" do
|
25
|
+
it "does nothing" do
|
26
|
+
expect(subject.load).to be nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#view" do
|
31
|
+
it "raises" do
|
32
|
+
expect { subject.view }.to raise_error ActionComponent::ViewMissingError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "private methods used by subclasses" do
|
37
|
+
class AuthorComponent < ActionComponent::Base
|
38
|
+
def view
|
39
|
+
div @author
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class TestComponent < ActionComponent::Base
|
44
|
+
def view
|
45
|
+
div(class: "post") do
|
46
|
+
h2 @post.title
|
47
|
+
|
48
|
+
div datetime_formatter(@post.posted_at), class: "datetime"
|
49
|
+
|
50
|
+
render "some_view"
|
51
|
+
|
52
|
+
component AuthorComponent, author: @post.author
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
let(:post) do
|
58
|
+
double(
|
59
|
+
title: "Test Post",
|
60
|
+
author: "Roger Nesbitt",
|
61
|
+
posted_at: Time.parse("2016-11-28 11:09:20"),
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
let(:view) { FakeView.new }
|
66
|
+
|
67
|
+
it "renders the component" do
|
68
|
+
result = TestComponent.render(view, post: post)
|
69
|
+
expect(result).to be nil
|
70
|
+
|
71
|
+
expect(view.calls).to eq [
|
72
|
+
[:content_tag, "div", {:class=>"post"}],
|
73
|
+
[:content_tag, "h2", "Test Post", nil],
|
74
|
+
[:concat, "content_tag [\"h2\", \"Test Post\", nil]"],
|
75
|
+
[:content_tag, "div", "28 November 2016 11:09", {:class=>"datetime"}],
|
76
|
+
[:concat, "content_tag [\"div\", \"28 November 2016 11:09\", {:class=>\"datetime\"}]"],
|
77
|
+
[:render, "some_view"],
|
78
|
+
[:concat, "render [\"some_view\"]"],
|
79
|
+
[:content_tag, "div", "Roger Nesbitt", nil],
|
80
|
+
[:concat, "content_tag [\"div\", \"Roger Nesbitt\", nil]"],
|
81
|
+
[:concat, "content_tag [\"div\", {:class=>\"post\"}]"],
|
82
|
+
]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActionComponent::Constraints do
|
4
|
+
class ConstraintsTest
|
5
|
+
include ActionComponent::Constraints
|
6
|
+
|
7
|
+
required :apple, :banana, carrot: String
|
8
|
+
optional durian: Hash
|
9
|
+
|
10
|
+
def check(opts)
|
11
|
+
check_constraints!(opts)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
subject { ConstraintsTest.new }
|
16
|
+
|
17
|
+
it "passes if all required attributes are supplied" do
|
18
|
+
subject.check(apple: 1, banana: 2, carrot: "hello", other: "ignored")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "requires required attributes" do
|
22
|
+
expect { subject.check(apple: 1) }.to raise_error(ActionComponent::ConstraintError, "banana is required for component ConstraintsTest")
|
23
|
+
|
24
|
+
expect { subject.check(apple: 1, banana: 1) }.to raise_error(ActionComponent::ConstraintError, "carrot is required for component ConstraintsTest")
|
25
|
+
end
|
26
|
+
|
27
|
+
it "enforces type for required attributes" do
|
28
|
+
expect { subject.check(apple: 1, banana: 1, carrot: 1) }.to raise_error(ActionComponent::ConstraintError, "carrot must be a String")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "enforces type for optional attributes" do
|
32
|
+
expect { subject.check(apple: 1, banana: 1, carrot: "hello", durian: 1) }.to raise_error(ActionComponent::ConstraintError, "durian must be a Hash")
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActionComponent::Elements do
|
4
|
+
class ElementsTest
|
5
|
+
include ActionComponent::Elements
|
6
|
+
|
7
|
+
define_tags :custom, :interesting
|
8
|
+
|
9
|
+
def initialize(view)
|
10
|
+
@_view = view
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:view) { FakeView.new }
|
15
|
+
|
16
|
+
subject { ElementsTest.new(view) }
|
17
|
+
|
18
|
+
describe "::define_tags" do
|
19
|
+
it "creates methods for the defined tags" do
|
20
|
+
expect(subject.respond_to?(:custom, true)).to be true
|
21
|
+
expect(subject.respond_to?(:interesting, true)).to be true
|
22
|
+
expect(subject.respond_to?(:does_not_exist, true)).to be false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#text" do
|
27
|
+
it "concats the supplied text" do
|
28
|
+
subject.send(:text, "some content")
|
29
|
+
|
30
|
+
expect(view.calls).to eq [[:concat, 'some content']]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#doctype" do
|
35
|
+
it "concats a doctype tag" do
|
36
|
+
subject.send(:doctype)
|
37
|
+
|
38
|
+
expect(view.calls).to eq [[:concat, '<!doctype html>']]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "concats a doctype tag with custom text" do
|
42
|
+
subject.send(:doctype, "<test>")
|
43
|
+
|
44
|
+
expect(view.calls).to eq [[:concat, '<!doctype <test>>']]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "an example of a define tag" do
|
49
|
+
it "calls element with its tag name as the first parameter" do
|
50
|
+
subject.send(:div, "div text", attribute: 'value')
|
51
|
+
|
52
|
+
expect(view.calls).to eq [
|
53
|
+
[:content_tag, "div", "div text", {:attribute=>"value"}],
|
54
|
+
[:concat, "content_tag [\"div\", \"div text\", {:attribute=>\"value\"}]"]
|
55
|
+
]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#element" do
|
60
|
+
context "when no arguments are specified" do
|
61
|
+
it "makes a tag" do
|
62
|
+
subject.send(:element, :name)
|
63
|
+
|
64
|
+
expect(view.calls).to eq [
|
65
|
+
[:tag, :name, nil],
|
66
|
+
[:concat, "tag [:name, nil]"],
|
67
|
+
]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when only one non-hash argument is specified" do
|
72
|
+
it "makes a content_tag with that argument" do
|
73
|
+
subject.send(:element, :name, "text")
|
74
|
+
|
75
|
+
expect(view.calls).to eq [
|
76
|
+
[:content_tag, :name, 'text', nil],
|
77
|
+
[:concat, "content_tag [:name, \"text\", nil]"],
|
78
|
+
]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "when only one hash argument is specified" do
|
83
|
+
it "makes a tag using attributes from that argument" do
|
84
|
+
subject.send(:element, :name, blue: 'very')
|
85
|
+
|
86
|
+
expect(view.calls.first).to eq [:tag, :name, {blue: 'very'}]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "when two arguments are specified" do
|
91
|
+
it "makes a content_tag using the text and attributes" do
|
92
|
+
subject.send(:element, :name, 'text', blue: 'very')
|
93
|
+
|
94
|
+
expect(view.calls.first).to eq [:content_tag, :name, 'text', {blue: 'very'}]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "when one argument and a block is specified" do
|
99
|
+
it "makes a content_tag with the attributes and yields" do
|
100
|
+
called = false
|
101
|
+
subject.send(:element, :name, blue: 'very') { called = true }
|
102
|
+
|
103
|
+
expect(called).to be true
|
104
|
+
expect(view.calls.first).to eq [:content_tag, :name, {blue: 'very'}]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "when text and a block are specified" do
|
109
|
+
it "raises" do
|
110
|
+
expect {
|
111
|
+
subject.send(:element, :name, 'hello') { 'yielded' }
|
112
|
+
}.to raise_error ActionComponent::RenderError
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/spec/examples.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
example_id | status | run_time |
|
2
|
+
------------------------------------------------- | ------ | --------------- |
|
3
|
+
./spec/action_component/base_spec.rb[1:1:1] | passed | 0.01317 seconds |
|
4
|
+
./spec/action_component/base_spec.rb[1:2:1] | passed | 0.00018 seconds |
|
5
|
+
./spec/action_component/base_spec.rb[1:3:1] | passed | 0.00012 seconds |
|
6
|
+
./spec/action_component/base_spec.rb[1:4:1] | passed | 0.00139 seconds |
|
7
|
+
./spec/action_component/constraints_spec.rb[1:1] | passed | 0.00006 seconds |
|
8
|
+
./spec/action_component/constraints_spec.rb[1:2] | passed | 0.00012 seconds |
|
9
|
+
./spec/action_component/constraints_spec.rb[1:3] | passed | 0.00017 seconds |
|
10
|
+
./spec/action_component/constraints_spec.rb[1:4] | passed | 0.00202 seconds |
|
11
|
+
./spec/action_component/elements_spec.rb[1:1:1] | passed | 0.00017 seconds |
|
12
|
+
./spec/action_component/elements_spec.rb[1:2:1] | passed | 0.00009 seconds |
|
13
|
+
./spec/action_component/elements_spec.rb[1:3:1] | passed | 0.00015 seconds |
|
14
|
+
./spec/action_component/elements_spec.rb[1:3:2] | passed | 0.00448 seconds |
|
15
|
+
./spec/action_component/elements_spec.rb[1:4:1] | passed | 0.00014 seconds |
|
16
|
+
./spec/action_component/elements_spec.rb[1:5:1:1] | passed | 0.00015 seconds |
|
17
|
+
./spec/action_component/elements_spec.rb[1:5:2:1] | passed | 0.0001 seconds |
|
18
|
+
./spec/action_component/elements_spec.rb[1:5:3:1] | passed | 0.00019 seconds |
|
19
|
+
./spec/action_component/elements_spec.rb[1:5:4:1] | passed | 0.00014 seconds |
|
20
|
+
./spec/action_component/elements_spec.rb[1:5:5:1] | passed | 0.00148 seconds |
|
21
|
+
./spec/action_component/elements_spec.rb[1:5:6:1] | passed | 0.00024 seconds |
|
data/spec/fake_view.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
class FakeView
|
2
|
+
attr_reader :calls
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@calls = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def datetime_formatter(time)
|
9
|
+
time.strftime("%d %B %Y %H:%M")
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(method, *args)
|
13
|
+
@calls << [method, *args]
|
14
|
+
|
15
|
+
yield if block_given?
|
16
|
+
|
17
|
+
"#{method} #{args.inspect}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require_relative '../lib/action_component'
|
2
|
+
require_relative "fake_view"
|
3
|
+
|
4
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
5
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
6
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
7
|
+
# this file to always be loaded, without a need to explicitly require it in any
|
8
|
+
# files.
|
9
|
+
#
|
10
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
11
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
12
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
13
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
14
|
+
# a separate helper file that requires the additional dependencies and performs
|
15
|
+
# the additional setup, and require it from the spec files that actually need
|
16
|
+
# it.
|
17
|
+
#
|
18
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
19
|
+
# users commonly want.
|
20
|
+
#
|
21
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
22
|
+
RSpec.configure do |config|
|
23
|
+
# rspec-expectations config goes here. You can use an alternate
|
24
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
25
|
+
# assertions if you prefer.
|
26
|
+
config.expect_with :rspec do |expectations|
|
27
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
28
|
+
# and `failure_message` of custom matchers include text for helper methods
|
29
|
+
# defined using `chain`, e.g.:
|
30
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
31
|
+
# # => "be bigger than 2 and smaller than 4"
|
32
|
+
# ...rather than:
|
33
|
+
# # => "be bigger than 2"
|
34
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
35
|
+
end
|
36
|
+
|
37
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
38
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
39
|
+
config.mock_with :rspec do |mocks|
|
40
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
41
|
+
# a real object. This is generally recommended, and will default to
|
42
|
+
# `true` in RSpec 4.
|
43
|
+
mocks.verify_partial_doubles = true
|
44
|
+
end
|
45
|
+
|
46
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
47
|
+
# have no way to turn it off -- the option exists only for backwards
|
48
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
49
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
50
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
51
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
52
|
+
|
53
|
+
# This allows you to limit a spec run to individual examples or groups
|
54
|
+
# you care about by tagging them with `:focus` metadata. When nothing
|
55
|
+
# is tagged with `:focus`, all examples get run. RSpec also provides
|
56
|
+
# aliases for `it`, `describe`, and `context` that include `:focus`
|
57
|
+
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
58
|
+
config.filter_run_when_matching :focus
|
59
|
+
|
60
|
+
# Allows RSpec to persist some state between runs in order to support
|
61
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
62
|
+
# you configure your source control system to ignore this file.
|
63
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
64
|
+
|
65
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
66
|
+
# recommended. For more details, see:
|
67
|
+
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
68
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
69
|
+
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
70
|
+
config.disable_monkey_patching!
|
71
|
+
|
72
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
73
|
+
# be too noisy due to issues in dependencies.
|
74
|
+
config.warnings = true
|
75
|
+
|
76
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
77
|
+
# file, and it's useful to allow more verbose output when running an
|
78
|
+
# individual spec file.
|
79
|
+
if config.files_to_run.one?
|
80
|
+
# Use the documentation formatter for detailed output,
|
81
|
+
# unless a formatter has already been configured
|
82
|
+
# (e.g. via a command-line flag).
|
83
|
+
config.default_formatter = 'doc'
|
84
|
+
end
|
85
|
+
|
86
|
+
# Print the 10 slowest examples and example groups at the
|
87
|
+
# end of the spec run, to help surface which specs are running
|
88
|
+
# particularly slow.
|
89
|
+
# config.profile_examples = 10
|
90
|
+
|
91
|
+
# Run specs in random order to surface order dependencies. If you find an
|
92
|
+
# order dependency and want to debug it, you can fix the order by providing
|
93
|
+
# the seed, which is printed after each run.
|
94
|
+
# --seed 1234
|
95
|
+
config.order = :random
|
96
|
+
|
97
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
98
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
99
|
+
# test failures related to randomization by passing the same `--seed` value
|
100
|
+
# as the one that triggered the failure.
|
101
|
+
Kernel.srand config.seed
|
102
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_component
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roger Nesbitt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-11-
|
11
|
+
date: 2016-11-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: railties
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -61,14 +75,25 @@ extensions: []
|
|
61
75
|
extra_rdoc_files: []
|
62
76
|
files:
|
63
77
|
- ".gitignore"
|
78
|
+
- ".rspec"
|
79
|
+
- Gemfile
|
80
|
+
- Gemfile.lock
|
64
81
|
- README.md
|
65
82
|
- action_component.gemspec
|
66
83
|
- lib/action_component.rb
|
67
84
|
- lib/action_component/action_controller_rendering.rb
|
68
85
|
- lib/action_component/action_view_rendering.rb
|
69
86
|
- lib/action_component/base.rb
|
87
|
+
- lib/action_component/constraints.rb
|
88
|
+
- lib/action_component/elements.rb
|
70
89
|
- lib/action_component/railtie.rb
|
71
90
|
- lib/action_component/version.rb
|
91
|
+
- spec/action_component/base_spec.rb
|
92
|
+
- spec/action_component/constraints_spec.rb
|
93
|
+
- spec/action_component/elements_spec.rb
|
94
|
+
- spec/examples.txt
|
95
|
+
- spec/fake_view.rb
|
96
|
+
- spec/spec_helper.rb
|
72
97
|
homepage: https://github.com/mogest/action_component
|
73
98
|
licenses:
|
74
99
|
- MIT
|
@@ -93,4 +118,10 @@ rubygems_version: 2.5.1
|
|
93
118
|
signing_key:
|
94
119
|
specification_version: 4
|
95
120
|
summary: React-style components for Rails
|
96
|
-
test_files:
|
121
|
+
test_files:
|
122
|
+
- spec/action_component/base_spec.rb
|
123
|
+
- spec/action_component/constraints_spec.rb
|
124
|
+
- spec/action_component/elements_spec.rb
|
125
|
+
- spec/examples.txt
|
126
|
+
- spec/fake_view.rb
|
127
|
+
- spec/spec_helper.rb
|