actionview-component 1.7.0 → 1.17.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE +5 -8
- data/.github/PULL_REQUEST_TEMPLATE +2 -4
- data/.github/workflows/ruby_on_rails.yml +6 -2
- data/.gitignore +1 -0
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +150 -0
- data/CONTRIBUTING.md +10 -19
- data/Gemfile.lock +20 -5
- data/README.md +204 -248
- data/actionview-component.gemspec +10 -8
- data/{lib/railties/lib → app/controllers}/rails/components_controller.rb +18 -8
- data/docs/case-studies/jellyswitch.md +76 -0
- data/lib/action_view/component.rb +1 -20
- data/lib/action_view/component/base.rb +2 -246
- data/lib/action_view/component/preview.rb +1 -80
- data/lib/action_view/component/railtie.rb +1 -64
- data/lib/action_view/component/test_case.rb +1 -3
- data/lib/action_view/component/test_helpers.rb +1 -19
- data/lib/rails/generators/component/component_generator.rb +6 -12
- data/lib/rails/generators/component/templates/component.rb.tt +0 -4
- data/lib/rails/generators/erb/component_generator.rb +21 -0
- data/lib/rails/generators/erb/templates/component.html.erb.tt +1 -0
- data/lib/rails/generators/haml/component_generator.rb +21 -0
- data/lib/rails/generators/haml/templates/component.html.haml.tt +1 -0
- data/lib/rails/generators/rspec/component_generator.rb +1 -1
- data/lib/rails/generators/rspec/templates/component_spec.rb.tt +1 -1
- data/lib/rails/generators/slim/component_generator.rb +21 -0
- data/lib/rails/generators/slim/templates/component.html.slim.tt +1 -0
- data/lib/rails/generators/test_unit/component_generator.rb +1 -1
- data/lib/rails/generators/test_unit/templates/component_test.rb.tt +3 -3
- data/lib/railties/lib/rails.rb +0 -1
- data/lib/railties/lib/rails/templates/rails/components/preview.html.erb +1 -1
- data/lib/view_component.rb +30 -0
- data/lib/view_component/base.rb +279 -0
- data/lib/view_component/conversion.rb +9 -0
- data/lib/view_component/engine.rb +65 -0
- data/lib/view_component/preview.rb +78 -0
- data/lib/view_component/previewable.rb +25 -0
- data/lib/view_component/render_monkey_patch.rb +31 -0
- data/lib/view_component/rendering_monkey_patch.rb +13 -0
- data/lib/view_component/template_error.rb +9 -0
- data/lib/view_component/test_case.rb +9 -0
- data/lib/view_component/test_helpers.rb +39 -0
- data/lib/view_component/version.rb +11 -0
- data/script/console +1 -1
- metadata +48 -21
- data/lib/action_view/component/conversion.rb +0 -11
- data/lib/action_view/component/previewable.rb +0 -27
- data/lib/action_view/component/render_monkey_patch.rb +0 -29
- data/lib/action_view/component/template_error.rb +0 -11
- data/lib/action_view/component/version.rb +0 -13
- data/lib/rails/generators/component/templates/component.html.erb.tt +0 -5
- data/lib/railties/lib/rails/component_examples_controller.rb +0 -9
- data/lib/railties/lib/rails/templates/rails/examples/show.html.erb +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24d6994186bb4c3e5f6fb8c628a3d91124cc6bdcbad61b966365c9170ab3b901
|
4
|
+
data.tar.gz: 020ab981225e9bb34508583c2aff096b125e43bee8c5ecc935ab52c6a22eb014
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 608b7e73a07c3b24c1fa273f65c1dea5f1e5d993f3db8ff3c3dde2d9d6a207604141849686cd78f4a6ac7a4a6c45567d81fb3eb054c7bccf4802565a3eb83a71
|
7
|
+
data.tar.gz: 8376ee07b5534e81b25c6241a0e9da6d02cf13a0b8b3b4036cdb6a062695897efaefd75a779f5d5d7009477ab03524da13f7a52fc557484dfd128cf77927e3f3
|
data/.github/ISSUE_TEMPLATE
CHANGED
@@ -6,17 +6,12 @@
|
|
6
6
|
### Motivation
|
7
7
|
|
8
8
|
<!-- What would you like to do with this feature? Can you provide
|
9
|
-
context or references to similar behavior in other libraries
|
9
|
+
context or references to similar behavior in other libraries? -->
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
<!-- **** Filing a Bug Report? Include these sections. **** -->
|
11
|
+
<!-- **** Filing a Bug Report? Include these sections: **** -->
|
16
12
|
|
17
13
|
### Steps to reproduce
|
18
|
-
<!-- Provide
|
19
|
-
demonstrate the bug you've identified. -->
|
14
|
+
<!-- Provide a series of steps or, better yet, a link to a repo to demonstrate the bug you've identified. -->
|
20
15
|
|
21
16
|
### Expected behavior
|
22
17
|
<!-- Tell us what should happen -->
|
@@ -28,3 +23,5 @@ demonstrate the bug you've identified. -->
|
|
28
23
|
**Rails version**:
|
29
24
|
|
30
25
|
**Ruby version**:
|
26
|
+
|
27
|
+
**Gem version**:
|
@@ -1,4 +1,4 @@
|
|
1
|
-
<!-- https://github.com/github/
|
1
|
+
<!-- See https://github.com/github/view_component/blob/master/CONTRIBUTING.md#submitting-a-pull-request -->
|
2
2
|
|
3
3
|
### Summary
|
4
4
|
|
@@ -14,6 +14,4 @@ request, mention that information here. This could include
|
|
14
14
|
benchmarks, or other information.
|
15
15
|
|
16
16
|
If you are updating any of the CHANGELOG files or are asked to update the
|
17
|
-
CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the file.
|
18
|
-
|
19
|
-
Thanks for contributing to actionview-component! -->
|
17
|
+
CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the file. -->
|
@@ -8,7 +8,7 @@ jobs:
|
|
8
8
|
strategy:
|
9
9
|
matrix:
|
10
10
|
rails_version: [5.0.0, 5.2.3, 6.0.0, master]
|
11
|
-
ruby_version: [2.4.x, 2.5.x, 2.6.x]
|
11
|
+
ruby_version: [2.4.x, 2.5.x, 2.6.x, 2.7.x]
|
12
12
|
exclude:
|
13
13
|
- rails_version: master
|
14
14
|
ruby_version: 2.4.x
|
@@ -20,9 +20,13 @@ jobs:
|
|
20
20
|
uses: actions/setup-ruby@v1
|
21
21
|
with:
|
22
22
|
ruby-version: ${{ matrix.ruby_version }}
|
23
|
+
- name: Update rubygems when testing with Ruby 2.4.x
|
24
|
+
if: startsWith(matrix.ruby_version, '2.4')
|
25
|
+
run: |
|
26
|
+
gem update --system --no-document
|
23
27
|
- name: Build and test with Rake
|
24
28
|
run: |
|
25
|
-
gem install bundler:1.
|
29
|
+
gem install bundler:1.17.3
|
26
30
|
bundle update
|
27
31
|
bundle install --jobs 4 --retry 3
|
28
32
|
bundle exec rake
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,153 @@
|
|
1
|
+
# master
|
2
|
+
|
3
|
+
# v1.17.0
|
4
|
+
|
5
|
+
* Support Ruby 2.4 in CI.
|
6
|
+
|
7
|
+
*Andrew Mason*
|
8
|
+
|
9
|
+
* ViewComponent generators do not not prompt for content requirement.
|
10
|
+
|
11
|
+
*Joel Hawksley*
|
12
|
+
|
13
|
+
* Add post-install message that gem has been renamed to `view_component`.
|
14
|
+
|
15
|
+
*Joel Hawksley*
|
16
|
+
|
17
|
+
# v1.16.0
|
18
|
+
|
19
|
+
* Add `refute_component_rendered` test helper.
|
20
|
+
|
21
|
+
*Joel Hawksley*
|
22
|
+
|
23
|
+
* Check for Rails before invocation.
|
24
|
+
|
25
|
+
*Dave Paola*
|
26
|
+
|
27
|
+
* Allow components to be rendered without a template file (aka inline component).
|
28
|
+
|
29
|
+
*Rainer Borene*
|
30
|
+
|
31
|
+
# v1.15.0
|
32
|
+
|
33
|
+
* Re-introduce ActionView::Component::TestHelpers.
|
34
|
+
|
35
|
+
*Joel Hawksley*
|
36
|
+
|
37
|
+
* Bypass monkey patch on Rails 6.1 builds.
|
38
|
+
|
39
|
+
*Joel Hawksley*
|
40
|
+
|
41
|
+
* Make `ActionView::Helpers::TagHelper` available in Previews.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
def with_html_content
|
45
|
+
render(MyComponent.new) do
|
46
|
+
tag.div do
|
47
|
+
content_tag(:span, "Hello")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
*Sean Doyle*
|
54
|
+
|
55
|
+
# v1.14.1
|
56
|
+
|
57
|
+
* Fix bug where generator created invalid test code.
|
58
|
+
|
59
|
+
*Joel Hawksley*
|
60
|
+
|
61
|
+
# v1.14.0
|
62
|
+
|
63
|
+
* Rename ActionView::Component::Base to ViewComponent::Base
|
64
|
+
|
65
|
+
*Joel Hawksley*
|
66
|
+
|
67
|
+
# v1.13.0
|
68
|
+
|
69
|
+
* Allow components to be rendered inside controllers.
|
70
|
+
|
71
|
+
*Joel Hawksley*
|
72
|
+
|
73
|
+
* Improve backtraces from exceptions raised in templates.
|
74
|
+
|
75
|
+
*Blake Williams*
|
76
|
+
|
77
|
+
# v1.12.0
|
78
|
+
|
79
|
+
* Revert: Remove initializer requirement for Ruby 2.7+
|
80
|
+
|
81
|
+
*Joel Hawksley*
|
82
|
+
|
83
|
+
* Restructure Railtie into Engine
|
84
|
+
|
85
|
+
*Sean Doyle*
|
86
|
+
|
87
|
+
* Allow components to override before_render_check
|
88
|
+
|
89
|
+
*Joel Hawksley*
|
90
|
+
|
91
|
+
# v1.11.1
|
92
|
+
|
93
|
+
* Relax Capybara requirement.
|
94
|
+
|
95
|
+
*Joel Hawksley*
|
96
|
+
|
97
|
+
# v1.11.0
|
98
|
+
|
99
|
+
* Add support for Capybara matchers.
|
100
|
+
|
101
|
+
*Joel Hawksley*
|
102
|
+
|
103
|
+
* Add erb, haml, & slim template generators
|
104
|
+
|
105
|
+
*Asger Behncke Jacobsen*
|
106
|
+
|
107
|
+
# v1.10.0
|
108
|
+
|
109
|
+
* Deprecate all `render` syntaxes except for `render(MyComponent.new(foo: :bar))`
|
110
|
+
|
111
|
+
*Joel Hawksley*
|
112
|
+
|
113
|
+
# v1.9.0
|
114
|
+
|
115
|
+
* Remove initializer requirement for Ruby 2.7+
|
116
|
+
|
117
|
+
*Dylan Clark*
|
118
|
+
|
119
|
+
# v1.8.1
|
120
|
+
|
121
|
+
* Run validation checks before calling `#render?`.
|
122
|
+
|
123
|
+
*Ash Wilson*
|
124
|
+
|
125
|
+
# v1.8.0
|
126
|
+
|
127
|
+
* Remove the unneeded ComponentExamplesController and simplify preview rendering.
|
128
|
+
|
129
|
+
*Jon Palmer*
|
130
|
+
|
131
|
+
* Add `#render?` hook to easily allow components to be no-ops.
|
132
|
+
|
133
|
+
*Kyle Fox*
|
134
|
+
|
135
|
+
* Don't assume ApplicationController exists.
|
136
|
+
|
137
|
+
*Jon Palmer*
|
138
|
+
|
139
|
+
* Allow some additional checks to overrided render?
|
140
|
+
|
141
|
+
*Sergey Malykh*
|
142
|
+
|
143
|
+
* Fix generator placing namespaced components in the root directory.
|
144
|
+
|
145
|
+
*Asger Behncke Jacobsen*
|
146
|
+
|
147
|
+
* Fix cache test.
|
148
|
+
|
149
|
+
*Sergey Malykh*
|
150
|
+
|
1
151
|
# v1.7.0
|
2
152
|
|
3
153
|
* Simplify validation of templates and compilation.
|
data/CONTRIBUTING.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
|
1
|
+
# Contributing
|
2
2
|
|
3
|
-
[fork]: https://github.com/github/
|
4
|
-
[pr]: https://github.com/github/
|
3
|
+
[fork]: https://github.com/github/view_component/fork
|
4
|
+
[pr]: https://github.com/github/view_component/compare
|
5
5
|
[style]: https://github.com/styleguide/ruby
|
6
6
|
[code-of-conduct]: CODE_OF_CONDUCT.md
|
7
7
|
|
@@ -15,9 +15,11 @@ Please note that this project is released with a [Contributor Code of Conduct][c
|
|
15
15
|
|
16
16
|
0. [Fork][fork] and clone the repository
|
17
17
|
0. Configure and install the dependencies: `bundle`
|
18
|
-
0. Make sure the tests pass on your machine: `rake`
|
18
|
+
0. Make sure the tests pass on your machine: `bundle exec rake`
|
19
19
|
0. Create a new branch: `git checkout -b my-branch-name`
|
20
20
|
0. Make your change, add tests, and make sure the tests still pass
|
21
|
+
0. Add an entry to the top of `CHANGELOG.md` for your changes
|
22
|
+
0. If it's your first time contributing, add yourself to the contributors at the bottom of `README.md`
|
21
23
|
0. Push to your fork and [submit a pull request][pr]
|
22
24
|
0. Pat your self on the back and wait for your pull request to be reviewed and merged.
|
23
25
|
|
@@ -32,24 +34,13 @@ Here are a few things you can do that will increase the likelihood of your pull
|
|
32
34
|
If you are the current maintainer of this gem:
|
33
35
|
|
34
36
|
1. Create a branch for the release: `git checkout -b release-vxx.xx.xx`
|
35
|
-
1. Bump gem version in `lib/action_view/component/version.rb`.
|
37
|
+
1. Bump gem version in `lib/action_view/component/version.rb`. Try to adhere to SemVer.
|
36
38
|
1. Add version heading/entries to `CHANGELOG.md`.
|
37
39
|
1. Make sure your local dependencies are up to date: `bundle`
|
38
40
|
1. Ensure that tests are green: `bundle exec rake`
|
39
|
-
1.
|
40
|
-
1. Test the test gem:
|
41
|
-
1. Bump the Gemfile and Gemfile.lock versions for an app which relies on this gem
|
42
|
-
1. Install the new gem locally
|
43
|
-
1. Test behavior locally, branch deploy, whatever needs to happen
|
44
|
-
1. Make a PR to github/actionview-component.
|
41
|
+
1. Make a PR to github/view_component.
|
45
42
|
1. Build a local gem: `gem build actionview-component.gemspec`
|
46
|
-
1. Merge github/
|
43
|
+
1. Merge github/view_component PR
|
47
44
|
1. Tag and push: `git tag vx.xx.xx; git push --tags`
|
48
|
-
1. Create a GitHub release with the pushed tag (https://github.com/github/
|
45
|
+
1. Create a GitHub release with the pushed tag (https://github.com/github/view_component/releases/new) and populate it with a list of the commits from `git log --pretty=format:"- %s" --reverse refs/tags/[OLD TAG]...refs/tags/[NEW TAG]`
|
49
46
|
1. Push to rubygems.org -- `gem push actionview-component-VERSION.gem`
|
50
|
-
|
51
|
-
## Resources
|
52
|
-
|
53
|
-
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
54
|
-
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
55
|
-
- [GitHub Help](https://help.github.com)
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
actionview-component (1.
|
4
|
+
actionview-component (1.17.0)
|
5
|
+
capybara (~> 3)
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
@@ -61,6 +62,8 @@ GEM
|
|
61
62
|
minitest (~> 5.1)
|
62
63
|
tzinfo (~> 1.1)
|
63
64
|
zeitwerk (~> 2.1, >= 2.1.8)
|
65
|
+
addressable (2.7.0)
|
66
|
+
public_suffix (>= 2.0.2, < 5.0)
|
64
67
|
ast (2.4.0)
|
65
68
|
better_html (1.0.14)
|
66
69
|
actionview (>= 4.0)
|
@@ -71,6 +74,14 @@ GEM
|
|
71
74
|
parser (>= 2.4)
|
72
75
|
smart_properties
|
73
76
|
builder (3.2.3)
|
77
|
+
capybara (3.31.0)
|
78
|
+
addressable
|
79
|
+
mini_mime (>= 0.1.3)
|
80
|
+
nokogiri (~> 1.8)
|
81
|
+
rack (>= 1.6.0)
|
82
|
+
rack-test (>= 0.6.3)
|
83
|
+
regexp_parser (~> 1.5)
|
84
|
+
xpath (~> 3.2)
|
74
85
|
concurrent-ruby (1.1.5)
|
75
86
|
crass (1.0.5)
|
76
87
|
erubi (1.8.0)
|
@@ -96,11 +107,12 @@ GEM
|
|
96
107
|
mini_portile2 (2.4.0)
|
97
108
|
minitest (5.1.0)
|
98
109
|
nio4r (2.5.2)
|
99
|
-
nokogiri (1.10.
|
110
|
+
nokogiri (1.10.8)
|
100
111
|
mini_portile2 (~> 2.4.0)
|
101
112
|
parallel (1.17.0)
|
102
113
|
parser (2.6.3.0)
|
103
114
|
ast (~> 2.4.0)
|
115
|
+
public_suffix (4.0.3)
|
104
116
|
rack (2.0.8)
|
105
117
|
rack-test (1.1.0)
|
106
118
|
rack (>= 1.0, < 3)
|
@@ -131,7 +143,8 @@ GEM
|
|
131
143
|
rake (>= 0.8.7)
|
132
144
|
thor (>= 0.20.3, < 2.0)
|
133
145
|
rainbow (3.0.0)
|
134
|
-
rake (
|
146
|
+
rake (13.0.1)
|
147
|
+
regexp_parser (1.7.0)
|
135
148
|
rubocop (0.74.0)
|
136
149
|
jaro_winkler (~> 1.5.1)
|
137
150
|
parallel (~> 1.10)
|
@@ -166,6 +179,8 @@ GEM
|
|
166
179
|
websocket-driver (0.7.1)
|
167
180
|
websocket-extensions (>= 0.1.0)
|
168
181
|
websocket-extensions (0.1.4)
|
182
|
+
xpath (3.2.0)
|
183
|
+
nokogiri (~> 1.8)
|
169
184
|
zeitwerk (2.1.10)
|
170
185
|
|
171
186
|
PLATFORMS
|
@@ -174,11 +189,11 @@ PLATFORMS
|
|
174
189
|
DEPENDENCIES
|
175
190
|
actionview-component!
|
176
191
|
better_html (~> 1)
|
177
|
-
bundler (
|
192
|
+
bundler (~> 1.14)
|
178
193
|
haml (~> 5)
|
179
194
|
minitest (= 5.1.0)
|
180
195
|
rails (= 6.0.0)
|
181
|
-
rake (~>
|
196
|
+
rake (~> 13.0)
|
182
197
|
rubocop (= 0.74)
|
183
198
|
rubocop-github (~> 0.13.0)
|
184
199
|
slim (~> 4.0)
|
data/README.md
CHANGED
@@ -1,111 +1,105 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# ViewComponent
|
2
|
+
A view component framework for Rails.
|
3
3
|
|
4
|
-
**Current Status**: Used in production at GitHub. Because of this, all changes will be thoroughly vetted, which could slow down the process of contributing. We will do our best to actively communicate status of pull requests with any contributors. If you have any substantial changes that you would like to make, it would be great to first [open an issue](http://github.com/github/
|
4
|
+
**Current Status**: Used in production at GitHub. Because of this, all changes will be thoroughly vetted, which could slow down the process of contributing. We will do our best to actively communicate status of pull requests with any contributors. If you have any substantial changes that you would like to make, it would be great to first [open an issue](http://github.com/github/view_component/issues/new) to discuss them with us.
|
5
|
+
|
6
|
+
## Migration in progress
|
7
|
+
|
8
|
+
This gem is in the process of a name / API change from `ActionView::Component` to `ViewComponent`, see https://github.com/github/view_component/issues/206.
|
9
|
+
|
10
|
+
### What's changing in the migration
|
11
|
+
|
12
|
+
1. `ActionView::Component::Base` is now `ViewComponent::Base`.
|
13
|
+
1. Components can only be rendered with `render(MyComponent.new)` syntax.
|
14
|
+
1. Validations are no longer supported by default.
|
15
|
+
|
16
|
+
### How to migrate to ViewComponent
|
17
|
+
|
18
|
+
1. In `application.rb`, require `view_component/engine`
|
19
|
+
1. Update components to inherit from `ViewComponent::Base`.
|
20
|
+
1. Update component tests to inherit from `ViewComponent::TestCase`.
|
21
|
+
1. Update component previews to inherit from `ViewComponent::Preview`.
|
22
|
+
1. Include `ViewComponent::TestHelpers` in the appropriate test helper file.
|
5
23
|
|
6
24
|
## Roadmap
|
7
25
|
|
8
|
-
|
26
|
+
Support for third-party component frameworks was merged into Rails `6.1.0.alpha` in https://github.com/rails/rails/pull/36388 and https://github.com/rails/rails/pull/37919. Our goal with this project is to provide a first-class component framework for this new capability in Rails.
|
9
27
|
|
10
|
-
|
28
|
+
This gem includes a backport of those changes for Rails `5.0.0` through `6.1.0.alpha`.
|
11
29
|
|
12
30
|
## Design philosophy
|
13
31
|
|
14
|
-
|
32
|
+
This library is designed to integrate as seamlessly as possible with Rails, with the [least surprise](https://www.artima.com/intv/ruby4.html).
|
15
33
|
|
16
34
|
## Compatibility
|
17
35
|
|
18
|
-
`actionview-component` is tested for compatibility with combinations of Ruby `2.4`/`2.5`/`2.6` and Rails `5.0.0`/`5.2.3`/`6.0.0`/`
|
36
|
+
`actionview-component` is tested for compatibility with combinations of Ruby `2.4`/`2.5`/`2.6`/`2.7` and Rails `5.0.0`/`5.2.3`/`6.0.0`/`master`.
|
19
37
|
|
20
38
|
## Installation
|
21
|
-
|
39
|
+
|
40
|
+
In `Gemfile`, add:
|
22
41
|
|
23
42
|
```ruby
|
24
43
|
gem "actionview-component"
|
25
44
|
```
|
26
45
|
|
27
|
-
And then execute:
|
28
|
-
```bash
|
29
|
-
$ bundle
|
30
|
-
```
|
31
|
-
|
32
46
|
In `config/application.rb`, add:
|
33
47
|
|
34
48
|
```bash
|
35
|
-
require "
|
49
|
+
require "view_component/engine"
|
36
50
|
```
|
37
51
|
|
38
52
|
## Guide
|
39
53
|
|
40
54
|
### What are components?
|
41
55
|
|
42
|
-
`
|
56
|
+
`ViewComponent`s are Ruby classes that are used to render views. They take data as input and return output-safe HTML. Think of them as an evolution of the presenter/decorator/view model pattern, inspired by [React Components](https://reactjs.org/docs/react-component.html).
|
43
57
|
|
44
|
-
|
58
|
+
Components are most effective in cases where view code is reused or benefits from being tested directly.
|
45
59
|
|
46
|
-
|
60
|
+
### Why should I use components?
|
47
61
|
|
48
62
|
#### Testing
|
49
63
|
|
50
|
-
|
64
|
+
Rails encourages testing views with integration tests. This discourages us from testing views thoroughly, due to the overhead of exercising the routing and controller layers in addition to the view.
|
51
65
|
|
52
|
-
|
66
|
+
For partials, this means being tested for each view they are included in, reducing the benefit of reusing them.
|
53
67
|
|
54
|
-
|
68
|
+
`ViewComponent`s can be unit-tested. In the GitHub codebase, our component unit tests run in around 25 milliseconds, compared to about six seconds for integration tests.
|
55
69
|
|
56
70
|
#### Data Flow
|
57
71
|
|
58
|
-
Unlike a method declaration on an object, views do not declare the values they are expected to receive, making it hard to figure out what context is necessary to render them. This often leads to subtle bugs when
|
59
|
-
|
60
|
-
#### Standards
|
72
|
+
Unlike a method declaration on an object, views do not declare the values they are expected to receive, making it hard to figure out what context is necessary to render them. This often leads to subtle bugs when reusing a view in different contexts.
|
61
73
|
|
62
|
-
|
74
|
+
By clearly defining the context necessary to render a `ViewComponent`, they're easier to reuse than partials.
|
63
75
|
|
64
|
-
|
76
|
+
#### Standards
|
65
77
|
|
66
|
-
|
78
|
+
Views often fail basic Ruby code quality standards: long methods, deep conditional nesting, and mystery guests abound.
|
67
79
|
|
68
|
-
`
|
80
|
+
`ViewComponent`s are Ruby objects, making it easy to follow code quality standards.
|
69
81
|
|
70
82
|
#### Code Coverage
|
71
83
|
|
72
|
-
|
84
|
+
Many common Ruby code coverage tools cannot properly handle coverage of views, making it difficult to audit how thorough tests are and leading to missing coverage in test suites.
|
73
85
|
|
74
|
-
|
75
|
-
|
76
|
-
By clearly defining the context necessary to render a component, we’ve found them to be easier to reuse than partials.
|
77
|
-
|
78
|
-
#### Performance
|
79
|
-
|
80
|
-
In early benchmarks, we’ve seen performance improvements over the existing rendering pipeline. For a test page with nested renders 10 levels deep, we’re seeing around a 5x increase in speed over partials:
|
81
|
-
|
82
|
-
```
|
83
|
-
Comparison:
|
84
|
-
component: 6515.4 i/s
|
85
|
-
partial: 1251.2 i/s - 5.21x slower
|
86
|
-
```
|
87
|
-
|
88
|
-
_Rails 6.1.0.alpha, [joelhawksley/actionview-component-demo](https://github.com/joelhawksley/actionview-component-demo), /benchmark route, via `RAILS_ENV=production rails s`, measured with [evanphx/benchmark-ips](https://github.com/evanphx/benchmark-ips)_
|
89
|
-
|
90
|
-
### When should I use components?
|
91
|
-
|
92
|
-
Components are most effective in cases where view code is reused or needs to be tested directly.
|
86
|
+
`ViewComponent` is at least partially compatible with code coverage tools, such as SimpleCov.
|
93
87
|
|
94
88
|
### Building components
|
95
89
|
|
96
|
-
|
90
|
+
#### Conventions
|
91
|
+
|
92
|
+
Components are subclasses of `ViewComponent::Base` and live in `app/components`. It's recommended to create an `ApplicationComponent` that is a subclass of `ViewComponent::Base` and inherit from that instead.
|
97
93
|
|
98
94
|
Component class names end in -`Component`.
|
99
95
|
|
100
96
|
Component module names are plural, as they are for controllers. (`Users::AvatarComponent`)
|
101
97
|
|
102
|
-
|
103
|
-
|
104
|
-
Content passed to an `ActionView::Component` as a block is captured and assigned to the `content` accessor.
|
98
|
+
Content passed to a `ViewComponent` as a block is captured and assigned to the `content` accessor.
|
105
99
|
|
106
100
|
#### Quick start
|
107
101
|
|
108
|
-
Use the component generator to create a new `
|
102
|
+
Use the component generator to create a new `ViewComponent`.
|
109
103
|
|
110
104
|
The generator accepts the component name and the list of accepted properties as arguments:
|
111
105
|
|
@@ -117,34 +111,36 @@ bin/rails generate component Example title content
|
|
117
111
|
create app/components/example_component.html.erb
|
118
112
|
```
|
119
113
|
|
114
|
+
`ViewComponent` includes template generators for the `erb`, `haml`, and `slim` template engines and will use the template engine specified in the Rails configuration (`config.generators.template_engine`) by default.
|
115
|
+
|
116
|
+
The template engine can also be passed as an option to the generator:
|
117
|
+
|
118
|
+
```bash
|
119
|
+
bin/rails generate component Example title content --template-engine slim
|
120
|
+
```
|
121
|
+
|
120
122
|
#### Implementation
|
121
123
|
|
122
|
-
|
124
|
+
A `ViewComponent` is a Ruby file and corresponding template file with the same base name:
|
123
125
|
|
124
126
|
`app/components/test_component.rb`:
|
125
127
|
```ruby
|
126
|
-
class TestComponent <
|
127
|
-
validates :content, :title, presence: true
|
128
|
-
|
128
|
+
class TestComponent < ViewComponent::Base
|
129
129
|
def initialize(title:)
|
130
130
|
@title = title
|
131
131
|
end
|
132
|
-
|
133
|
-
private
|
134
|
-
|
135
|
-
attr_reader :title
|
136
132
|
end
|
137
133
|
```
|
138
134
|
|
139
135
|
`app/components/test_component.html.erb`:
|
140
136
|
```erb
|
141
|
-
<span title="<%= title %>"><%= content %></span>
|
137
|
+
<span title="<%= @title %>"><%= content %></span>
|
142
138
|
```
|
143
139
|
|
144
|
-
|
140
|
+
Which is rendered in a view as:
|
145
141
|
|
146
142
|
```erb
|
147
|
-
<%= render(TestComponent
|
143
|
+
<%= render(TestComponent.new(title: "my title")) do %>
|
148
144
|
Hello, World!
|
149
145
|
<% end %>
|
150
146
|
```
|
@@ -155,64 +151,18 @@ Which returns:
|
|
155
151
|
<span title="my title">Hello, World!</span>
|
156
152
|
```
|
157
153
|
|
158
|
-
|
159
|
-
|
160
|
-
Components can be rendered via:
|
161
|
-
|
162
|
-
`render(TestComponent, foo: :bar)`
|
163
|
-
|
164
|
-
`render(component: TestComponent, locals: { foo: :bar })`
|
165
|
-
|
166
|
-
**Rendering components through models**
|
167
|
-
|
168
|
-
Passing model instances will cause `render` to look for its respective component class.
|
169
|
-
|
170
|
-
The component is instantiated with the rendered model instance.
|
171
|
-
|
172
|
-
Example for a `Post` model:
|
173
|
-
|
174
|
-
`render(@post)`
|
175
|
-
|
176
|
-
```ruby
|
177
|
-
class PostComponent < ActionView::Component::Base
|
178
|
-
def initialize(post)
|
179
|
-
@post = post
|
180
|
-
end
|
181
|
-
end
|
182
|
-
```
|
183
|
-
|
184
|
-
The following syntax has been deprecated and will be removed in v2.0.0:
|
185
|
-
|
186
|
-
`render(TestComponent.new(foo: :bar))`
|
187
|
-
|
188
|
-
#### Error case
|
189
|
-
|
190
|
-
If the component is rendered with a blank title:
|
191
|
-
|
192
|
-
```erb
|
193
|
-
<%= render(TestComponent, title: "") do %>
|
194
|
-
Hello, World!
|
195
|
-
<% end %>
|
196
|
-
```
|
197
|
-
|
198
|
-
An error will be raised:
|
199
|
-
|
200
|
-
`ActiveModel::ValidationError: Validation failed: Title can't be blank`
|
154
|
+
`ViewComponent` requires the presence of an `initialize` method in each component.
|
201
155
|
|
202
156
|
#### Content Areas
|
203
157
|
|
204
|
-
|
205
158
|
A component can declare additional content areas to be rendered in the component. For example:
|
206
159
|
|
207
160
|
`app/components/modal_component.rb`:
|
208
161
|
```ruby
|
209
|
-
class ModalComponent <
|
210
|
-
validates :user, :header, :body, presence: true
|
211
|
-
|
162
|
+
class ModalComponent < ViewComponent::Base
|
212
163
|
with_content_areas :header, :body
|
213
164
|
|
214
|
-
def initialize(
|
215
|
-
@user = user
|
165
|
+
def initialize(*)
|
216
166
|
end
|
217
167
|
end
|
218
168
|
```
|
@@ -221,16 +171,16 @@ end
|
|
221
171
|
```erb
|
222
172
|
<div class="modal">
|
223
173
|
<div class="header"><%= header %></div>
|
224
|
-
<div class="body"><%= body
|
174
|
+
<div class="body"><%= body %></div>
|
225
175
|
</div>
|
226
176
|
```
|
227
177
|
|
228
|
-
|
178
|
+
Which is rendered in a view as:
|
229
179
|
|
230
180
|
```erb
|
231
|
-
<%= render(ModalComponent
|
181
|
+
<%= render(ModalComponent.new) do |component| %>
|
232
182
|
<% component.with(:header) do %>
|
233
|
-
Hello
|
183
|
+
Hello Jane
|
234
184
|
<% end %>
|
235
185
|
<% component.with(:body) do %>
|
236
186
|
<p>Have a great day.</p>
|
@@ -247,223 +197,190 @@ Which returns:
|
|
247
197
|
</div>
|
248
198
|
```
|
249
199
|
|
250
|
-
|
251
|
-
This allows a few different combinations of ways to render the component:
|
200
|
+
### Inline Component
|
252
201
|
|
253
|
-
|
254
|
-
|
255
|
-
`app/components/modal_component.rb`:
|
256
|
-
```ruby
|
257
|
-
class ModalComponent < ActionView::Component::Base
|
258
|
-
validates :header, :body, presence: true
|
202
|
+
A component can be rendered without any template file as well.
|
259
203
|
|
260
|
-
|
204
|
+
`app/components/inline_component.rb`:
|
261
205
|
|
262
|
-
|
263
|
-
|
206
|
+
```ruby
|
207
|
+
class InlineComponent < ViewComponent::Base
|
208
|
+
def call
|
209
|
+
if active?
|
210
|
+
link_to "Cancel integration", integration_path, method: :delete
|
211
|
+
else
|
212
|
+
link_to "Integrate now!", integration_path
|
213
|
+
end
|
264
214
|
end
|
265
215
|
end
|
266
216
|
```
|
267
217
|
|
268
|
-
|
269
|
-
<%= render(ModalComponent, header: "Hi!") do |component| %>
|
270
|
-
<% help_enabled? && component.with(:header) do %>
|
271
|
-
<span class="help_icon"><%= component.header %></span>
|
272
|
-
<% end %>
|
273
|
-
<% component.with(:body) do %>
|
274
|
-
<p>Have a great day.</p>
|
275
|
-
<% end %>
|
276
|
-
<% end %>
|
277
|
-
```
|
278
|
-
|
279
|
-
##### Required argument passed by render argument or by named block
|
218
|
+
### Conditional Rendering
|
280
219
|
|
281
|
-
|
282
|
-
```ruby
|
283
|
-
class ModalComponent < ActionView::Component::Base
|
284
|
-
validates :header, :body, presence: true
|
220
|
+
Components can implement a `#render?` method to determine if they should be rendered.
|
285
221
|
|
286
|
-
|
222
|
+
For example, given a component that displays a banner to users who haven't confirmed their email address, the logic for whether to render the banner would need to go in either the component template:
|
287
223
|
|
288
|
-
|
289
|
-
@header = header
|
290
|
-
end
|
291
|
-
end
|
224
|
+
`app/components/confirm_email_component.html.erb`
|
292
225
|
```
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
<% component.with(:body) do %>
|
298
|
-
<p>Have a great day.</p>
|
299
|
-
<% end %>
|
226
|
+
<% if user.requires_confirmation? %>
|
227
|
+
<div class="alert">
|
228
|
+
Please confirm your email address.
|
229
|
+
</div>
|
300
230
|
<% end %>
|
301
231
|
```
|
302
232
|
|
303
|
-
|
233
|
+
or the view that renders the component:
|
234
|
+
|
235
|
+
`app/views/_banners.html.erb`
|
304
236
|
```erb
|
305
|
-
|
306
|
-
|
307
|
-
<span class="help_icon">Hello</span>
|
308
|
-
<% end %>
|
309
|
-
<% component.with(:body) do %>
|
310
|
-
<p>Have a great day.</p>
|
311
|
-
<% end %>
|
237
|
+
<% if current_user.requires_confirmation? %>
|
238
|
+
<%= render(ConfirmEmailComponent.new(user: current_user)) %>
|
312
239
|
<% end %>
|
313
240
|
```
|
314
241
|
|
315
|
-
|
242
|
+
Instead, the `#render?` hook expresses this logic in the Ruby class, simplifying the view:
|
316
243
|
|
317
|
-
`app/components/
|
244
|
+
`app/components/confirm_email_component.rb`
|
318
245
|
```ruby
|
319
|
-
class
|
320
|
-
|
321
|
-
|
322
|
-
|
246
|
+
class ConfirmEmailComponent < ViewComponent::Base
|
247
|
+
def initialize(user:)
|
248
|
+
@user = user
|
249
|
+
end
|
323
250
|
|
324
|
-
def
|
325
|
-
@
|
251
|
+
def render?
|
252
|
+
@user.requires_confirmation?
|
326
253
|
end
|
327
254
|
end
|
328
255
|
```
|
329
256
|
|
330
|
-
`app/components/
|
331
|
-
```
|
332
|
-
<div class="
|
333
|
-
|
334
|
-
<div class="header"><%= header %></div>
|
335
|
-
<% end %>
|
336
|
-
<div class="body"><%= body %>"></div>
|
257
|
+
`app/components/confirm_email_component.html.erb`
|
258
|
+
```
|
259
|
+
<div class="banner">
|
260
|
+
Please confirm your email address.
|
337
261
|
</div>
|
338
262
|
```
|
339
263
|
|
340
|
-
`app/views/
|
264
|
+
`app/views/_banners.html.erb`
|
341
265
|
```erb
|
342
|
-
<%= render(
|
343
|
-
<% component.with(:body) do %>
|
344
|
-
<p>Have a great day.</p>
|
345
|
-
<% end %>
|
346
|
-
<% end %>
|
266
|
+
<%= render(ConfirmEmailComponent.new(user: current_user)) %>
|
347
267
|
```
|
348
268
|
|
349
|
-
`
|
350
|
-
```erb
|
351
|
-
<%= render(ModalComponent) do |component| %>
|
352
|
-
<% component.with(:header) do %>
|
353
|
-
<span class="help_icon">Hello</span>
|
354
|
-
<% end %>
|
355
|
-
<% component.with(:body) do %>
|
356
|
-
<p>Have a great day.</p>
|
357
|
-
<% end %>
|
358
|
-
<% end %>
|
359
|
-
```
|
360
|
-
|
361
|
-
`app/views/no_header.html.erb`:
|
362
|
-
```erb
|
363
|
-
<%= render(ModalComponent) do |component| %>
|
364
|
-
<% component.with(:body) do %>
|
365
|
-
<p>Have a great day.</p>
|
366
|
-
<% end %>
|
367
|
-
<% end %>
|
368
|
-
```
|
269
|
+
To assert that a component has not been rendered, use `refute_component_rendered` from `ViewComponent::TestHelpers`.
|
369
270
|
|
370
271
|
### Testing
|
371
272
|
|
372
|
-
|
273
|
+
Unit test components directly, using the `render_inline` test helper and Capybara matchers:
|
373
274
|
|
374
275
|
```ruby
|
375
|
-
require "
|
276
|
+
require "view_component/test_case"
|
376
277
|
|
377
|
-
class MyComponentTest <
|
278
|
+
class MyComponentTest < ViewComponent::TestCase
|
378
279
|
test "render component" do
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
)
|
280
|
+
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
|
281
|
+
|
282
|
+
assert_selector("span[title='my title']", "Hello, World!")
|
383
283
|
end
|
384
284
|
end
|
385
285
|
```
|
386
286
|
|
387
|
-
In general, we’ve found it makes the most sense to test components based on their rendered HTML.
|
388
|
-
|
389
287
|
#### Action Pack Variants
|
390
288
|
|
391
|
-
|
289
|
+
Use the `with_variant` helper to test specific variants:
|
392
290
|
|
393
291
|
```ruby
|
394
292
|
test "render component for tablet" do
|
395
293
|
with_variant :tablet do
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
)
|
294
|
+
render_inline(TestComponent.new(title: "my title")) { "Hello, tablets!" }
|
295
|
+
|
296
|
+
assert_selector("span[title='my title']", "Hello, tablets!")
|
400
297
|
end
|
401
298
|
end
|
402
299
|
```
|
403
300
|
|
404
301
|
### Previewing Components
|
405
|
-
`
|
406
|
-
In the previous example, the preview class for `TestComponent` would be called `TestComponentPreview` and located in `test/components/previews/test_component_preview.rb`.
|
407
|
-
To see the preview of the component with a given title, implement a method that renders the component.
|
408
|
-
You can define as many examples as you want:
|
302
|
+
`ViewComponent::Preview`, like `ActionMailer::Preview`, provides a way to preview components in isolation:
|
409
303
|
|
304
|
+
`test/components/previews/test_component_preview.rb`
|
410
305
|
```ruby
|
411
|
-
|
412
|
-
|
413
|
-
class TestComponentPreview < ActionView::Component::Preview
|
306
|
+
class TestComponentPreview < ViewComponent::Preview
|
414
307
|
def with_default_title
|
415
|
-
render(TestComponent
|
308
|
+
render(TestComponent.new(title: "Test component default"))
|
416
309
|
end
|
417
310
|
|
418
311
|
def with_long_title
|
419
|
-
render(TestComponent
|
312
|
+
render(TestComponent.new(title: "This is a really long title to see how the component renders this"))
|
313
|
+
end
|
314
|
+
|
315
|
+
def with_content_block
|
316
|
+
render(TestComponent.new(title: "This component accepts a block of content") do
|
317
|
+
tag.div do
|
318
|
+
content_tag(:span, "Hello")
|
319
|
+
end
|
320
|
+
end
|
420
321
|
end
|
421
322
|
end
|
422
323
|
```
|
423
324
|
|
424
|
-
|
425
|
-
|
325
|
+
Which generates <http://localhost:3000/rails/components/test_component/with_default_title>,
|
326
|
+
<http://localhost:3000/rails/components/test_component/with_long_title>,
|
327
|
+
and <http://localhost:3000/rails/components/test_component/with_content_block>.
|
426
328
|
|
427
|
-
|
329
|
+
The `ViewComponent::Preview` base class includes
|
330
|
+
[`ActionView::Helpers::TagHelper`][tag-helper], which provides the [`tag`][tag]
|
331
|
+
and [`content_tag`][content_tag] view helper methods.
|
428
332
|
|
429
|
-
|
430
|
-
|
333
|
+
[tag-helper]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html
|
334
|
+
[tag]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag
|
335
|
+
[content_tag]: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag
|
431
336
|
|
432
|
-
|
337
|
+
Previews default to the application layout, but can be overridden:
|
338
|
+
|
339
|
+
`test/components/previews/test_component_preview.rb`
|
340
|
+
```ruby
|
341
|
+
class TestComponentPreview < ViewComponent::Preview
|
433
342
|
layout "admin"
|
434
343
|
|
435
344
|
...
|
436
345
|
end
|
437
346
|
```
|
438
347
|
|
439
|
-
|
440
|
-
|
441
|
-
|
348
|
+
Preview classes live in `test/components/previews`, can be configured using the `preview_path` option.
|
349
|
+
|
350
|
+
To use `lib/component_previews`:
|
442
351
|
|
352
|
+
`config/application.rb`
|
443
353
|
```ruby
|
444
354
|
config.action_view_component.preview_path = "#{Rails.root}/lib/component_previews"
|
445
355
|
```
|
446
356
|
|
357
|
+
#### Configuring TestController
|
358
|
+
|
359
|
+
Component tests and previews assume the existence of an `ApplicationController` class, be can beconfigured using the `test_controller` option:
|
360
|
+
|
361
|
+
`config/application.rb`
|
362
|
+
```ruby
|
363
|
+
config.action_view_component.test_controller = "BaseController"
|
364
|
+
```
|
365
|
+
|
447
366
|
### Setting up RSpec
|
448
367
|
|
449
|
-
|
450
|
-
`spec/rails_helper.rb`:
|
368
|
+
To use RSpec, add the following:
|
451
369
|
|
370
|
+
`spec/rails_helper.rb`
|
452
371
|
```ruby
|
453
|
-
require "
|
372
|
+
require "view_component/test_helpers"
|
454
373
|
|
455
374
|
RSpec.configure do |config|
|
456
|
-
|
457
|
-
|
458
|
-
# Ensure that the test helpers are available in component specs
|
459
|
-
config.include ActionView::Component::TestHelpers, type: :component
|
375
|
+
config.include ViewComponent::TestHelpers, type: :component
|
460
376
|
end
|
461
377
|
```
|
462
378
|
|
463
|
-
Specs created by the generator
|
379
|
+
Specs created by the generator have access to test helpers like `render_inline`.
|
464
380
|
|
465
|
-
To use component previews
|
381
|
+
To use component previews:
|
466
382
|
|
383
|
+
`config/application.rb`
|
467
384
|
```ruby
|
468
385
|
config.action_view_component.preview_path = "#{Rails.root}/spec/components/previews"
|
469
386
|
```
|
@@ -480,7 +397,7 @@ Inline templates have been removed (for now) due to concerns raised by [@soutaro
|
|
480
397
|
|
481
398
|
### Isn't this just like X library?
|
482
399
|
|
483
|
-
`
|
400
|
+
`ViewComponent` is far from a novel idea! Popular implementations of view components in Ruby include, but are not limited to:
|
484
401
|
|
485
402
|
- [trailblazer/cells](https://github.com/trailblazer/cells)
|
486
403
|
- [dry-rb/dry-view](https://github.com/dry-rb/dry-view)
|
@@ -493,11 +410,50 @@ Inline templates have been removed (for now) due to concerns raised by [@soutaro
|
|
493
410
|
- [Introducing ActionView::Component with Joel Hawksley, Ruby on Rails Podcast](http://5by5.tv/rubyonrails/276)
|
494
411
|
- [Rails to Introduce View Components, Dev.to](https://dev.to/andy/rails-to-introduce-view-components-3ome)
|
495
412
|
- [ActionView::Components in Rails 6.1, Drifting Ruby](https://www.driftingruby.com/episodes/actionview-components-in-rails-6-1)
|
496
|
-
- [Demo repository,
|
413
|
+
- [Demo repository, view-component-demo](https://github.com/joelhawksley/view-component-demo)
|
497
414
|
|
498
415
|
## Contributing
|
499
416
|
|
500
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/github/
|
417
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/github/view_component. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. We recommend reading the [contributing guide](./CONTRIBUTING.md) as well.
|
418
|
+
|
419
|
+
## Contributors
|
420
|
+
|
421
|
+
`actionview-component` is built by:
|
422
|
+
|
423
|
+
|<img src="https://avatars.githubusercontent.com/joelhawksley?s=256" alt="joelhawksley" width="128" />|<img src="https://avatars.githubusercontent.com/tenderlove?s=256" alt="tenderlove" width="128" />|<img src="https://avatars.githubusercontent.com/jonspalmer?s=256" alt="jonspalmer" width="128" />|<img src="https://avatars.githubusercontent.com/juanmanuelramallo?s=256" alt="juanmanuelramallo" width="128" />|<img src="https://avatars.githubusercontent.com/vinistock?s=256" alt="vinistock" width="128" />|
|
424
|
+
|:---:|:---:|:---:|:---:|:---:|
|
425
|
+
|@joelhawksley|@tenderlove|@jonspalmer|@juanmanuelramallo|@vinistock|
|
426
|
+
|Denver|Seattle|Boston||Toronto|
|
427
|
+
|
428
|
+
|<img src="https://avatars.githubusercontent.com/metade?s=256" alt="metade" width="128" />|<img src="https://avatars.githubusercontent.com/asgerb?s=256" alt="asgerb" width="128" />|<img src="https://avatars.githubusercontent.com/xronos-i-am?s=256" alt="xronos-i-am" width="128" />|<img src="https://avatars.githubusercontent.com/dylnclrk?s=256" alt="dylnclrk" width="128" />|<img src="https://avatars.githubusercontent.com/kaspermeyer?s=256" alt="kaspermeyer" width="128" />|
|
429
|
+
|:---:|:---:|:---:|:---:|:---:|
|
430
|
+
|@metade|@asgerb|@xronos-i-am|@dylnclrk|@kaspermeyer|
|
431
|
+
|London|Copenhagen|Russia, Kirov|Berkeley, CA|Denmark|
|
432
|
+
|
433
|
+
|<img src="https://avatars.githubusercontent.com/rdavid1099?s=256" alt="rdavid1099" width="128" />|<img src="https://avatars.githubusercontent.com/kylefox?s=256" alt="kylefox" width="128" />|<img src="https://avatars.githubusercontent.com/traels?s=256" alt="traels" width="128" />|<img src="https://avatars.githubusercontent.com/rainerborene?s=256" alt="rainerborene" width="128" />|<img src="https://avatars.githubusercontent.com/jcoyne?s=256" alt="jcoyne" width="128" />|
|
434
|
+
|:---:|:---:|:---:|:---:|:---:|
|
435
|
+
|@rdavid1099|@kylefox|@traels|@rainerborene|@jcoyne|
|
436
|
+
|Los Angeles|Edmonton|Odense, Denmark|Brazil|Minneapolis|
|
437
|
+
|
438
|
+
|<img src="https://avatars.githubusercontent.com/elia?s=256" alt="elia" width="128" />|<img src="https://avatars.githubusercontent.com/cesariouy?s=256" alt="cesariouy" width="128" />|<img src="https://avatars.githubusercontent.com/spdawson?s=256" alt="spdawson" width="128" />|<img src="https://avatars.githubusercontent.com/rmacklin?s=256" alt="rmacklin" width="128" />|<img src="https://avatars.githubusercontent.com/michaelem?s=256" alt="michaelem" width="128" />|
|
439
|
+
|:---:|:---:|:---:|:---:|:---:|
|
440
|
+
|@elia|@cesariouy|@spdawson|@rmacklin|@michaelem|
|
441
|
+
|Milan||United Kingdom||Berlin|
|
442
|
+
|
443
|
+
|<img src="https://avatars.githubusercontent.com/mellowfish?s=256" alt="mellowfish" width="128" />|<img src="https://avatars.githubusercontent.com/horacio?s=256" alt="horacio" width="128" />|<img src="https://avatars.githubusercontent.com/dukex?s=256" alt="dukex" width="128" />|<img src="https://avatars.githubusercontent.com/dark-panda?s=256" alt="dark-panda" width="128" />|<img src="https://avatars.githubusercontent.com/smashwilson?s=256" alt="smashwilson" width="128" />|
|
444
|
+
|:---:|:---:|:---:|:---:|:---:|
|
445
|
+
|@mellowfish|@horacio|@dukex|@dark-panda|@smashwilson|
|
446
|
+
|Spring Hill, TN|Buenos Aires|São Paulo||Gambrills, MD|
|
447
|
+
|
448
|
+
|<img src="https://avatars.githubusercontent.com/blakewilliams?s=256" alt="blakewilliams" width="128" />|
|
449
|
+
|:---:|
|
450
|
+
|@blakewilliams|
|
451
|
+
|Boston, MA|
|
452
|
+
|
453
|
+
|<img src="https://avatars.githubusercontent.com/seanpdoyle?s=256" alt="seanpdoyle" width="128" />|
|
454
|
+
|:---:|
|
455
|
+
|@seanpdoyle|
|
456
|
+
|New York, NY|
|
501
457
|
|
502
458
|
## License
|
503
459
|
|