component_party 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate +23 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.rubocop.yml +24 -0
- data/.rubocop_todo.yml +0 -0
- data/.travis.yml +36 -0
- data/.yardopts +3 -0
- data/Gemfile +18 -0
- data/README.md +455 -0
- data/component_party.gemspec +32 -0
- data/lib/component_party/action_view/component_renderer/tag_wrapper_decorator.rb +26 -0
- data/lib/component_party/action_view/component_renderer.rb +54 -0
- data/lib/component_party/action_view/renderer.rb +36 -0
- data/lib/component_party/importer_helper.rb +34 -0
- data/lib/component_party/railtie.rb +16 -0
- data/lib/component_party/view_model.rb +17 -0
- data/lib/component_party.rb +34 -0
- data/partyhard.gif +0 -0
- data/rainbow_meme_v1.jpg +0 -0
- metadata +209 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 73fbc5bb12f3bbbd726ca0314afeac72fddf416b0adf9c0a324c74722675349d
|
4
|
+
data.tar.gz: 251c72e7dac7ad1328ba42742fa3439cfeb69b009efd1937ef981bcdf7a90aa1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 46b22b5d9a78f1927117e331ddcd90d07806e77f8118fe46683c281767485805ac30731aa91d8f26a0aca6f37f81255ff3b2af433bc533582c1c6c5afc92abd3
|
7
|
+
data.tar.gz: eac0e6c9ca872d6ba997db86c52a869186bb9e4b528746b65bf35b2797a7e1932cad5a0228c6410c25f9f172bee58ce11cd5710abb5371fa7546ede646385a35
|
data/.codeclimate
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
engines:
|
2
|
+
rubocop:
|
3
|
+
enabled: true
|
4
|
+
#checks:
|
5
|
+
# Rubocop/Metrics/ClassLength:
|
6
|
+
# enabled: false
|
7
|
+
brakeman:
|
8
|
+
enabled: false
|
9
|
+
eslint:
|
10
|
+
enabled: false
|
11
|
+
csslint:
|
12
|
+
enabled: false
|
13
|
+
duplication:
|
14
|
+
enabled: true
|
15
|
+
config:
|
16
|
+
languages:
|
17
|
+
- ruby:
|
18
|
+
- javascript:
|
19
|
+
ratings:
|
20
|
+
paths:
|
21
|
+
- lib/**
|
22
|
+
exclude_paths:
|
23
|
+
- spec/**/*
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
Documentation:
|
4
|
+
Enabled: false
|
5
|
+
|
6
|
+
Style/ClassAndModuleChildren:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
Style/FrozenStringLiteralComment:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Metrics/LineLength:
|
13
|
+
Max: 150
|
14
|
+
IgnoredPatterns: ['\A#']
|
15
|
+
|
16
|
+
Metrics/MethodLength:
|
17
|
+
Max: 20
|
18
|
+
|
19
|
+
AllCops:
|
20
|
+
Exclude:
|
21
|
+
- component_party.gemspec
|
22
|
+
- 'exe/**/*'
|
23
|
+
- 'bin/**/*'
|
24
|
+
- 'spec/**/*'
|
data/.rubocop_todo.yml
ADDED
File without changes
|
data/.travis.yml
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
env:
|
2
|
+
global:
|
3
|
+
- CC_TEST_REPORTER_ID=2373c9fa7226b98ccf8bef72f133bc37b4cef57085c027e144a3b2fc71ce7558
|
4
|
+
matrix:
|
5
|
+
- "RAILS_VERSION=4.2.0"
|
6
|
+
- "RAILS_VERSION=5.0.0"
|
7
|
+
- "RAILS_VERSION=5.1.0"
|
8
|
+
- "RAILS_VERSION=5.2.0"
|
9
|
+
|
10
|
+
language: ruby
|
11
|
+
|
12
|
+
cache:
|
13
|
+
bundler: true
|
14
|
+
|
15
|
+
rvm:
|
16
|
+
- "2.4.1"
|
17
|
+
|
18
|
+
bundler_args: "--binstubs --standalone --without documentation --path ../bundle"
|
19
|
+
|
20
|
+
before_install:
|
21
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
22
|
+
- chmod +x ./cc-test-reporter
|
23
|
+
|
24
|
+
before_script:
|
25
|
+
- ./cc-test-reporter before-build
|
26
|
+
|
27
|
+
script:
|
28
|
+
- bundle exec rubocop -c .rubocop.yml
|
29
|
+
- bundle exec rspec
|
30
|
+
|
31
|
+
notifications:
|
32
|
+
email: false
|
33
|
+
|
34
|
+
after_script:
|
35
|
+
- if [[ "$TRAVIS_TEST_RESULT" == 0 ]]; then ./cc-test-reporter format-coverage -t simplecov -o ./coverage/codeclimate.$CI_NODE_INDEX.json ./coverage/spec/.resultset.json; fi
|
36
|
+
- if [[ "$TRAVIS_TEST_RESULT" == 0 ]]; then ./cc-test-reporter sum-coverage --output - --parts $CI_NODE_TOTAL coverage/codeclimate.*.json | ./cc-test-reporter upload-coverage --input -; fi
|
data/.yardopts
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'yard', '~> 0.9.9', require: false
|
4
|
+
|
5
|
+
rails_version = ENV['RAILS_VERSION'] || 'default'
|
6
|
+
|
7
|
+
rails = case rails_version
|
8
|
+
when 'master'
|
9
|
+
{ github: 'rails/rails' }
|
10
|
+
when 'default'
|
11
|
+
'~> 4.2.0'
|
12
|
+
else
|
13
|
+
"~> #{rails_version}"
|
14
|
+
end
|
15
|
+
|
16
|
+
gem 'rails', rails
|
17
|
+
# Specify your gem's dependencies in rabbitmq-spec.gemspec
|
18
|
+
gemspec
|
data/README.md
ADDED
@@ -0,0 +1,455 @@
|
|
1
|
+
# Welcome to component_party gem
|
2
|
+
|
3
|
+
[![Travis](https://travis-ci.com/viniciusoyama/component_party.svg?branch=master)](https://travis-ci.com/viniciusoyama/component_party)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/viniciusoyama/component_party/badges/gpa.svg)](https://codeclimate.com/github/viniciusoyama/component_party)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/viniciusoyama/component_party/badges/coverage.svg)](https://codeclimate.com/github/viniciusoyama/component_party)
|
6
|
+
|
7
|
+
Frontend components for Ruby on Rails: group your view logic, html and css files in components to be rendered from views or directly from controllers.
|
8
|
+
|
9
|
+
# Table of contents
|
10
|
+
|
11
|
+
<!-- TOC depthFrom:1 depthTo:1 withLinks:1 updateOnSave:0 orderedList:0 -->
|
12
|
+
|
13
|
+
- [Quick overview](#quick-overview)
|
14
|
+
- [Importing components](#importing-components)
|
15
|
+
- [View Models: pass data to your components](#view-models-pass-data-to-your-components)
|
16
|
+
- [Using helpers inside your components](#using-helpers-inside-your-components)
|
17
|
+
- [Accessing params and other controller's methods](#accessing-params-and-other-controllers-methods)
|
18
|
+
- [Rendering a component from a controller's action](#rendering-a-component-from-a-controllers-action)
|
19
|
+
- [Style namespacing: css scoped by component](#style-namespacing-css-scoped-by-component)
|
20
|
+
- [Configuration](#configuration)
|
21
|
+
|
22
|
+
<!-- /TOC -->
|
23
|
+
|
24
|
+
# Quick overview
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
Add to your gemfile: `gem 'component_party'`
|
28
|
+
|
29
|
+
## Using the gem
|
30
|
+
|
31
|
+
1) Move things to app/components folder and organize your frontend
|
32
|
+
|
33
|
+
|
34
|
+
```
|
35
|
+
app
|
36
|
+
├── components
|
37
|
+
│ └── sidebar
|
38
|
+
│ └── template.erb
|
39
|
+
│ └── style.sass
|
40
|
+
│ └── header
|
41
|
+
│ └── template.erb
|
42
|
+
│ └── style.sass
|
43
|
+
│ └── pages
|
44
|
+
│ └── users
|
45
|
+
│ └── index
|
46
|
+
│ └── template.erb
|
47
|
+
│ └── style.sass
|
48
|
+
│ └── filter
|
49
|
+
│ └── template.erb
|
50
|
+
│ └── view_model.rb
|
51
|
+
│ └── style.sass
|
52
|
+
│ └── list
|
53
|
+
│ └── template.erb
|
54
|
+
│ └── style.sass
|
55
|
+
```
|
56
|
+
|
57
|
+
|
58
|
+
2) Code your templates
|
59
|
+
|
60
|
+
**app/components/pages/users/index/template.erb**
|
61
|
+
|
62
|
+
```html
|
63
|
+
<%
|
64
|
+
import_component 'Filter', path: './filter'
|
65
|
+
import_component 'List', path: './list'
|
66
|
+
%>
|
67
|
+
|
68
|
+
<%= Filter() %>
|
69
|
+
|
70
|
+
<%= List(users: users) %>
|
71
|
+
```
|
72
|
+
|
73
|
+
**app/components/pages/users/list/template.erb**
|
74
|
+
|
75
|
+
```html
|
76
|
+
<table>
|
77
|
+
<tbody>
|
78
|
+
<% vm.users.each do |user| %>
|
79
|
+
<tr>
|
80
|
+
<td><%= user.name %></td>
|
81
|
+
</tr>
|
82
|
+
<% end %>
|
83
|
+
</tbody>
|
84
|
+
</table>
|
85
|
+
```
|
86
|
+
|
87
|
+
3) Render a component from your controller
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
|
91
|
+
class UsersController < ApplicationController
|
92
|
+
|
93
|
+
def index
|
94
|
+
render component: true, view_model_data: { users: User.all }
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
4) Customize and namespace your css for a given component
|
101
|
+
|
102
|
+
**app/components/pages/users/list/style.sass**
|
103
|
+
|
104
|
+
```sass
|
105
|
+
[data-component-path=pages-users-list] table tr
|
106
|
+
background: blue
|
107
|
+
```
|
108
|
+
|
109
|
+
5) Be astonished by what you've accomplished
|
110
|
+
|
111
|
+
![I'm so cool](rainbow_meme_v1.jpg?raw=true "Title")
|
112
|
+
![Party Hard](partyhard.gif?raw=true "Title")
|
113
|
+
|
114
|
+
# Importing components
|
115
|
+
|
116
|
+
```
|
117
|
+
app
|
118
|
+
├── components
|
119
|
+
│ └── application_layout
|
120
|
+
│ └── header
|
121
|
+
│ └── template.html.erb
|
122
|
+
│ └── user
|
123
|
+
│ └── panel
|
124
|
+
│ └── sidebar
|
125
|
+
│ └── template.html.erb
|
126
|
+
│ └── template.html.erb
|
127
|
+
```
|
128
|
+
|
129
|
+
You can import a component inside a layout, view or a component's template.
|
130
|
+
|
131
|
+
## Absolute importing
|
132
|
+
|
133
|
+
Just use the full component path.
|
134
|
+
|
135
|
+
**app/views/layouts/application.html.erb**
|
136
|
+
|
137
|
+
```html
|
138
|
+
|
139
|
+
<%
|
140
|
+
import_component 'Header', path: 'application_layout/header'
|
141
|
+
%>
|
142
|
+
|
143
|
+
<%= Header(my_user: current_user) %>
|
144
|
+
```
|
145
|
+
|
146
|
+
## Relative importing
|
147
|
+
|
148
|
+
Use "./" before component's path.
|
149
|
+
|
150
|
+
The next example will look for a `sidebar` component inside the app/components/user/panel folder.
|
151
|
+
|
152
|
+
**app/components/user/panel/sidebar.html.erb**
|
153
|
+
|
154
|
+
```html
|
155
|
+
|
156
|
+
<%
|
157
|
+
import_component 'Sidebar', path: './sidebar'
|
158
|
+
%>
|
159
|
+
|
160
|
+
<%= Sidebar() %>
|
161
|
+
```
|
162
|
+
|
163
|
+
# View Models: pass data to your components
|
164
|
+
|
165
|
+
While rendering a component, you can pass data in a hash format. The data will automatically be exposed to your template though a `vm` variable.
|
166
|
+
|
167
|
+
```
|
168
|
+
app
|
169
|
+
├── components
|
170
|
+
│ └── header
|
171
|
+
│ └── template.html.erb
|
172
|
+
```
|
173
|
+
|
174
|
+
Rendering the component and passing data:
|
175
|
+
|
176
|
+
```html
|
177
|
+
|
178
|
+
<%
|
179
|
+
import_component 'Header', path: 'header'
|
180
|
+
%>
|
181
|
+
|
182
|
+
<%= Header(my_user: current_user) %>
|
183
|
+
```
|
184
|
+
|
185
|
+
You have access to the my_user attribute in your component's template:
|
186
|
+
|
187
|
+
The component's template code:
|
188
|
+
|
189
|
+
```html
|
190
|
+
<p>Hello <%= vm.my_user.name %></p>
|
191
|
+
```
|
192
|
+
|
193
|
+
## How it works?
|
194
|
+
|
195
|
+
The `vm` variable is an instance of a ViewModel.
|
196
|
+
|
197
|
+
By default, we instantiate our `ComponentParty::ViewModel`. This class takes all the constructor arguments (it must be a hash/named args) and creates a getter for each one of them. Example:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
vm = ComponentParty::ViewModel.new(name: 'John', age: 12)
|
201
|
+
vm.name # John
|
202
|
+
vm.age # 12
|
203
|
+
```
|
204
|
+
|
205
|
+
When a view model is instantiated we pass the all arguments that you provided while calling your component in the template.
|
206
|
+
|
207
|
+
## Create your own ViewModel and handle complex view logic
|
208
|
+
|
209
|
+
Suppose that we want to use a custom view model (inside our header component's folder).
|
210
|
+
|
211
|
+
**app/components/header/view_model.rb**
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
class Header::ViewModel < ComponentParty::ViewModel
|
215
|
+
def random_greeting
|
216
|
+
hi_text = ['Hi', 'Yo'].sample
|
217
|
+
"#{hi_text}, #{user.name}"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
While importing our Header component we can pass a `custom_view_model` option to the import directive.
|
223
|
+
|
224
|
+
```html
|
225
|
+
<%
|
226
|
+
import_component 'Header', path: 'header', custom_view_model: true
|
227
|
+
%>
|
228
|
+
|
229
|
+
<%= Header(name: 'John') %>
|
230
|
+
```
|
231
|
+
|
232
|
+
By default we will use our own view model. But if you pass the `custom_view_model` options as `true` the rendering process will look for a class following all the Rails naming conventions.
|
233
|
+
|
234
|
+
Also, you can use any class as a view model. Instead of `true` just use the class itself:
|
235
|
+
|
236
|
+
```html
|
237
|
+
<%
|
238
|
+
import_component 'Header', path: 'header', custom_view_model: MyCustomClass
|
239
|
+
%>
|
240
|
+
|
241
|
+
<%= Header(name: 'John') %>
|
242
|
+
```
|
243
|
+
|
244
|
+
*PS:* You need to pass the class and not an instance of it.
|
245
|
+
|
246
|
+
Note that a view model *must* inherit from ComponentParty::ViewModel in order to be compliant to the internal API that a view model must have. Also, it is not expected that you override the `initialize` method in your custom view model.
|
247
|
+
|
248
|
+
**Is it possible to (by default) automatically search for a custom view model and, if not found, just use the default one instead?**
|
249
|
+
|
250
|
+
Yes, it would be possible to avoid the need of you passing a `custom_view_model` option but we don't like this approach for 2 main reasons:
|
251
|
+
|
252
|
+
1) This feature would make the rendering process much slower.
|
253
|
+
|
254
|
+
2) We think it's better to do things more explicitly for who is reading your code.
|
255
|
+
|
256
|
+
# Using helpers inside your components
|
257
|
+
|
258
|
+
The component's template is compiled using a normal rails view context so you have access to all helpers/params/routes/etc:
|
259
|
+
|
260
|
+
```
|
261
|
+
<div class="child">
|
262
|
+
<div class="date">
|
263
|
+
<%= l(Date.new(2019, 01, 03)) %>
|
264
|
+
</div>
|
265
|
+
|
266
|
+
<div class="routes">
|
267
|
+
<%= users_path %>
|
268
|
+
</div>
|
269
|
+
|
270
|
+
<div class="translation">
|
271
|
+
<%= t('hello')%>
|
272
|
+
</div>
|
273
|
+
</div>
|
274
|
+
```
|
275
|
+
|
276
|
+
If you want to to access it inside a view model, just use the `view` method.
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
class Header::ViewModel < ComponentParty::ViewModel
|
280
|
+
def formated_date
|
281
|
+
view.l(Date.today)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
```
|
285
|
+
|
286
|
+
**app/components/header/template.html.erb**
|
287
|
+
```html
|
288
|
+
<header>
|
289
|
+
Today is <%= vm.formated_date %>
|
290
|
+
</header>
|
291
|
+
```
|
292
|
+
|
293
|
+
|
294
|
+
# Accessing params and other controller's methods
|
295
|
+
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
class Header::ViewModel < ComponentParty::ViewModel
|
299
|
+
|
300
|
+
def formated_page
|
301
|
+
"Current page: #{view.params[:page]}"
|
302
|
+
end
|
303
|
+
|
304
|
+
def formated_search
|
305
|
+
"Searching for: #{view.params[:search]}"
|
306
|
+
end
|
307
|
+
|
308
|
+
def hello
|
309
|
+
"Hi #{view.current_user.name}"
|
310
|
+
end
|
311
|
+
|
312
|
+
end
|
313
|
+
```
|
314
|
+
|
315
|
+
**template.erb**
|
316
|
+
|
317
|
+
```html
|
318
|
+
<%= vm.hello %>
|
319
|
+
|
320
|
+
<%= vm.formated_search %>
|
321
|
+
|
322
|
+
<%= vm.formated_page %>
|
323
|
+
|
324
|
+
<%= params[:search] %>
|
325
|
+
```
|
326
|
+
|
327
|
+
# Rendering a component from a controller's action
|
328
|
+
|
329
|
+
When you are in an action you can render a component instead of a rails view using the following syntax:
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
|
333
|
+
class ClientsController < ApplicationController
|
334
|
+
|
335
|
+
def index
|
336
|
+
render(component: 'my/component/path', view_model_data: { new_arg: 2, more_arg: 'text'})
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
|
341
|
+
```
|
342
|
+
|
343
|
+
If you want to render the default component for an given action just do:
|
344
|
+
|
345
|
+
|
346
|
+
```ruby
|
347
|
+
class ClientsController < ApplicationController
|
348
|
+
|
349
|
+
def index
|
350
|
+
render component: true, view_model_data: { clients: Client.all }
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
This will search for a component with a path of `app/components/pages/clients/index`.
|
357
|
+
|
358
|
+
Note that we will add **pages** before the default controller+action path.
|
359
|
+
|
360
|
+
**Can I make the gem render a component instead of a view by default?**
|
361
|
+
|
362
|
+
We though about doing this but - even if the default behavior was to render a component instead of a view - you would have to pass the view model data in the action using some kind of trick like:
|
363
|
+
|
364
|
+
```
|
365
|
+
def index
|
366
|
+
set_view_model_attribute(:users, User.all)
|
367
|
+
end
|
368
|
+
```
|
369
|
+
|
370
|
+
Using the controller's instance variables just like views doesn't make sense in a ViewModel implementation.
|
371
|
+
|
372
|
+
Also, as we want to make things more explicitly (in case of another dev that doesn't know about this gem enters the project), it's better to always have to write the command.
|
373
|
+
|
374
|
+
```
|
375
|
+
render component: true
|
376
|
+
```
|
377
|
+
|
378
|
+
# Style namespacing: css scoped by component
|
379
|
+
|
380
|
+
Your template **must** have only one root node in order to your component be namespaced.
|
381
|
+
|
382
|
+
Each rendered component will have its first HTML node changed with a dynamic data attribute storing the component path. This means that you can create custom css for each component. Example:
|
383
|
+
|
384
|
+
```
|
385
|
+
app
|
386
|
+
├── components
|
387
|
+
│ └── admin_layout
|
388
|
+
│ └── header
|
389
|
+
│ └── template.html.erb
|
390
|
+
│ └── style.css
|
391
|
+
```
|
392
|
+
|
393
|
+
```html
|
394
|
+
<section>
|
395
|
+
<h1>Hello <%= vm.user_name%></h1>
|
396
|
+
</section>
|
397
|
+
```
|
398
|
+
|
399
|
+
When we render the header it will generate a HTML like this
|
400
|
+
|
401
|
+
```html
|
402
|
+
<section data-component-path='admin_layout-header'>
|
403
|
+
<h1>Hello <%= vm.user_name%></h1>
|
404
|
+
</section>
|
405
|
+
```
|
406
|
+
|
407
|
+
Then you can customize the component with the following css:
|
408
|
+
|
409
|
+
```css
|
410
|
+
[data-component-path=admin_layout-header] {
|
411
|
+
background: red;
|
412
|
+
}
|
413
|
+
```
|
414
|
+
|
415
|
+
## How to import the css files?
|
416
|
+
|
417
|
+
You can split your css in each component folder. It doesn't matter the name or the number of css/sass/less files that you have in each folder... Just don't forget to namespace it!
|
418
|
+
|
419
|
+
Also, in your application.css file you should require all the css from the app/components folder with a relative `require_tree`. Like this:
|
420
|
+
|
421
|
+
**application.sass**
|
422
|
+
|
423
|
+
```sass
|
424
|
+
//*=require_tree ../../../components
|
425
|
+
@import "fullcalendar.min"
|
426
|
+
@import "bootstrap"
|
427
|
+
@import "datepicker"
|
428
|
+
|
429
|
+
// ...
|
430
|
+
```
|
431
|
+
|
432
|
+
# Configuration
|
433
|
+
|
434
|
+
You can customize our gem by creating an initializer on your rails app.
|
435
|
+
|
436
|
+
**config/initializers/component_party.rb**
|
437
|
+
|
438
|
+
```ruby
|
439
|
+
|
440
|
+
ComponentParty.configure do |config|
|
441
|
+
# Folder path to look for components
|
442
|
+
config.components_path = 'app/components'
|
443
|
+
|
444
|
+
# Name for the html/erb/slim/etc template file inside the component folder
|
445
|
+
config.template_file_name = 'template'
|
446
|
+
|
447
|
+
# Name for the view model file inside the component folder
|
448
|
+
config.view_model_file_name = 'view_model'
|
449
|
+
|
450
|
+
# Folder path inside the components folder to look for pages when
|
451
|
+
# rendering the default component for a controller#action
|
452
|
+
config.component_folder_for_actions = 'pages'
|
453
|
+
end
|
454
|
+
|
455
|
+
```
|
@@ -0,0 +1,32 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'component_party'
|
3
|
+
s.version = '0.8.0'
|
4
|
+
s.date = '2019-03-28'
|
5
|
+
s.summary = 'Stop using views: frontend components architecture for Ruby on Rails.'
|
6
|
+
s.description = 'Frontend components for Ruby on Rails: group your view logic, html and css files in components to be rendered from views or directly from controllers.'
|
7
|
+
s.authors = ['Vinícius Oyama']
|
8
|
+
s.email = 'contact@viniciusoyama.com'
|
9
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
10
|
+
s.homepage =
|
11
|
+
'https://github.com/viniciusoyama/component_party'
|
12
|
+
s.license = 'MIT'
|
13
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
14
|
+
|
15
|
+
s.add_dependency 'rails', '>= 4.2.0', '< 6'
|
16
|
+
|
17
|
+
# Quality Control
|
18
|
+
s.add_development_dependency 'rspec'
|
19
|
+
s.add_development_dependency 'rubocop'
|
20
|
+
s.add_development_dependency 'simplecov'
|
21
|
+
|
22
|
+
# Debugging
|
23
|
+
s.add_development_dependency 'awesome_print'
|
24
|
+
s.add_development_dependency 'pry-byebug'
|
25
|
+
|
26
|
+
# for testing a gem with a rails app (controller specs)
|
27
|
+
# https://codingdaily.wordpress.com/2011/01/14/test-a-gem-with-the-rails-3-stack/
|
28
|
+
s.add_development_dependency 'bundler'
|
29
|
+
s.add_development_dependency 'capybara'
|
30
|
+
s.add_development_dependency 'rspec-rails'
|
31
|
+
s.add_development_dependency 'sqlite3'
|
32
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Decorates the template renderer chosen to render
|
2
|
+
# the current view/template/file.
|
3
|
+
# It overrides the render method in order to wrap the rendered template
|
4
|
+
# in a tag with data attributes
|
5
|
+
class ComponentParty::ActionView::ComponentRenderer::TagWrapperDecorator < SimpleDelegator
|
6
|
+
attr_reader :component_path
|
7
|
+
|
8
|
+
def initialize(target, component_path)
|
9
|
+
@component_path = component_path
|
10
|
+
super(target)
|
11
|
+
end
|
12
|
+
|
13
|
+
def render(context, options = {}, &block)
|
14
|
+
rendered = __getobj__.render(context, options, &block)
|
15
|
+
rendered = apply_html_namespacing(rendered.to_s)
|
16
|
+
ActionView::OutputBuffer.new(rendered)
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply_html_namespacing(raw_html)
|
20
|
+
component_id = component_path.to_s.gsub(%r{^/}, '').tr('/', '-')
|
21
|
+
html_tag_end_regex = %r{(/?)>}
|
22
|
+
raw_html.sub(html_tag_end_regex) do
|
23
|
+
" data-component-path='#{component_id}' #{Regexp.last_match(1)}>"
|
24
|
+
end.html_safe
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ComponentParty
|
2
|
+
# Renders a given component
|
3
|
+
module ActionView
|
4
|
+
class ComponentRenderer < ::ActionView::TemplateRenderer
|
5
|
+
attr_reader :lookup_context
|
6
|
+
attr_reader :component_path
|
7
|
+
|
8
|
+
def initialize(lookup_context, component_path)
|
9
|
+
@component_path = component_path
|
10
|
+
super(lookup_context)
|
11
|
+
end
|
12
|
+
|
13
|
+
def render(context, options)
|
14
|
+
options[:file] = template_path_from_component_path(options[:component])
|
15
|
+
options[:locals] = { vm: create_view_model(context, options) }
|
16
|
+
super(context, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def render_template(template, layout_name = nil, locals = nil) #:nodoc:
|
20
|
+
super(decorate_template(template), layout_name, locals)
|
21
|
+
end
|
22
|
+
|
23
|
+
def decorate_template(template)
|
24
|
+
ComponentParty::ActionView::ComponentRenderer::TagWrapperDecorator.new(template, component_path)
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_view_model(context, options)
|
28
|
+
view_model_data = options[:view_model_data] || {}
|
29
|
+
view_model_data[:view] = context
|
30
|
+
|
31
|
+
vm_class = find_vm_class(options)
|
32
|
+
|
33
|
+
vm_class.new(view_model_data)
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_vm_class(options)
|
37
|
+
if options[:custom_view_model]
|
38
|
+
if options[:custom_view_model] == true
|
39
|
+
vm_file_path = Pathname.new(options[:component]).join(ComponentParty.configuration.view_model_file_name).to_s
|
40
|
+
ActiveSupport::Inflector.camelize(vm_file_path).constantize
|
41
|
+
else
|
42
|
+
options[:custom_view_model]
|
43
|
+
end
|
44
|
+
else
|
45
|
+
ComponentParty::ViewModel
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def template_path_from_component_path(component_path, template_file_name: ComponentParty.configuration.template_file_name)
|
50
|
+
Pathname.new(component_path).join(template_file_name).to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ComponentParty #:nodoc:
|
2
|
+
# Renders a given component
|
3
|
+
module ActionView
|
4
|
+
module Renderer
|
5
|
+
def render(context, options)
|
6
|
+
if options.key?(:component)
|
7
|
+
normalize_data_for_component_rendering!(context, options)
|
8
|
+
ComponentParty::ActionView::ComponentRenderer.new(lookup_context, options[:component]).render(context, options)
|
9
|
+
else
|
10
|
+
super(context, options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def normalize_data_for_component_rendering!(context, options)
|
15
|
+
normalize_component_path!(context, options)
|
16
|
+
context.instance_variable_set('@current_component_path', options[:component])
|
17
|
+
end
|
18
|
+
|
19
|
+
# An example of options argumento passed by Rails are
|
20
|
+
# {
|
21
|
+
# :prefixes=>["devise/sessions", "devise", "application"],
|
22
|
+
# :template=>"new",
|
23
|
+
# :layout=> a Proc
|
24
|
+
# }
|
25
|
+
# rubocop:disable Metrics/LineLength
|
26
|
+
def normalize_component_path!(_context, options)
|
27
|
+
if options[:component] == true
|
28
|
+
options[:component] = Pathname.new(ComponentParty.configuration.component_folder_for_actions).join(options[:prefixes].first.to_s, options[:template]).to_s
|
29
|
+
else
|
30
|
+
options[:component]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
# rubocop:enable all
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ComponentParty
|
2
|
+
# Exposes component rendering methods to Rails views
|
3
|
+
|
4
|
+
module ImporterHelper
|
5
|
+
# Description of #import_component
|
6
|
+
#
|
7
|
+
# @param local_component_name [String] Local's component name (in the view scope)
|
8
|
+
# @param opts [Hash] default: {} Options.
|
9
|
+
# @example
|
10
|
+
def import_component(local_component_name, opts = {})
|
11
|
+
raise "No path informed when importing component #{local_component_name}" if opts[:path].blank?
|
12
|
+
|
13
|
+
component_to_render_path = get_full_component_path(opts[:path])
|
14
|
+
|
15
|
+
define_singleton_method(local_component_name) do |**args|
|
16
|
+
render(component: component_to_render_path, view_model_data: args, custom_view_model: (opts[:custom_view_model] || false))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def get_full_component_path(path)
|
23
|
+
if path.starts_with?('./')
|
24
|
+
current_component_path = instance_variable_get('@current_component_path')
|
25
|
+
|
26
|
+
raise "You cannot use a relative component importing outside a component's template." if current_component_path.blank?
|
27
|
+
|
28
|
+
Pathname.new(current_component_path).join(path).to_s
|
29
|
+
else
|
30
|
+
path
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
|
3
|
+
class ViewComponent
|
4
|
+
class Rails < Rails::Railtie
|
5
|
+
initializer 'view_component' do
|
6
|
+
config.assets.paths << ::Rails.root.join(ComponentParty.configuration.components_path)
|
7
|
+
|
8
|
+
ActiveSupport.on_load(:action_controller) do
|
9
|
+
prepend_view_path(::Rails.root.join(ComponentParty.configuration.components_path)) if respond_to?(:prepend_view_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
::ActionView::Renderer.send(:prepend, ComponentParty::ActionView::Renderer)
|
13
|
+
::ActionView::Base.send(:include, ComponentParty::ImporterHelper)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ComponentParty
|
2
|
+
class ViewModel
|
3
|
+
def initialize(**args)
|
4
|
+
generate_methods_from_hash(args)
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def generate_methods_from_hash(hash)
|
10
|
+
hash.each do |key, val|
|
11
|
+
define_singleton_method key.to_sym do
|
12
|
+
val
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'component_party/importer_helper'
|
2
|
+
require 'component_party/view_model'
|
3
|
+
require 'component_party/action_view/renderer'
|
4
|
+
require 'component_party/action_view/component_renderer'
|
5
|
+
require 'component_party/action_view/component_renderer/tag_wrapper_decorator'
|
6
|
+
|
7
|
+
module ComponentParty
|
8
|
+
# Configuration class for ComponentParty
|
9
|
+
# @attr components_path [String] Components folder path (defaults to 'app/components')
|
10
|
+
# @attr components_path [String] Component's template file name (defaults to 'template')
|
11
|
+
class Configuration
|
12
|
+
attr_accessor :components_path
|
13
|
+
attr_accessor :template_file_name
|
14
|
+
attr_accessor :view_model_file_name
|
15
|
+
attr_accessor :component_folder_for_actions
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@components_path = 'app/components'
|
19
|
+
@template_file_name = 'template'
|
20
|
+
@view_model_file_name = 'view_model'
|
21
|
+
@component_folder_for_actions = 'pages'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.configuration
|
26
|
+
@configuration ||= Configuration.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.configure
|
30
|
+
yield(configuration)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'component_party/railtie'
|
data/partyhard.gif
ADDED
Binary file
|
data/rainbow_meme_v1.jpg
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: component_party
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.8.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vinícius Oyama
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-03-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.0
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 4.2.0
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rspec
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rubocop
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: simplecov
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: awesome_print
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: pry-byebug
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: bundler
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: capybara
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: rspec-rails
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: sqlite3
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
description: 'Frontend components for Ruby on Rails: group your view logic, html and
|
160
|
+
css files in components to be rendered from views or directly from controllers.'
|
161
|
+
email: contact@viniciusoyama.com
|
162
|
+
executables: []
|
163
|
+
extensions: []
|
164
|
+
extra_rdoc_files: []
|
165
|
+
files:
|
166
|
+
- ".codeclimate"
|
167
|
+
- ".gitignore"
|
168
|
+
- ".rspec"
|
169
|
+
- ".rubocop.yml"
|
170
|
+
- ".rubocop_todo.yml"
|
171
|
+
- ".travis.yml"
|
172
|
+
- ".yardopts"
|
173
|
+
- Gemfile
|
174
|
+
- README.md
|
175
|
+
- component_party.gemspec
|
176
|
+
- lib/component_party.rb
|
177
|
+
- lib/component_party/action_view/component_renderer.rb
|
178
|
+
- lib/component_party/action_view/component_renderer/tag_wrapper_decorator.rb
|
179
|
+
- lib/component_party/action_view/renderer.rb
|
180
|
+
- lib/component_party/importer_helper.rb
|
181
|
+
- lib/component_party/railtie.rb
|
182
|
+
- lib/component_party/view_model.rb
|
183
|
+
- partyhard.gif
|
184
|
+
- rainbow_meme_v1.jpg
|
185
|
+
homepage: https://github.com/viniciusoyama/component_party
|
186
|
+
licenses:
|
187
|
+
- MIT
|
188
|
+
metadata: {}
|
189
|
+
post_install_message:
|
190
|
+
rdoc_options: []
|
191
|
+
require_paths:
|
192
|
+
- lib
|
193
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
194
|
+
requirements:
|
195
|
+
- - ">="
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
|
+
requirements:
|
200
|
+
- - ">="
|
201
|
+
- !ruby/object:Gem::Version
|
202
|
+
version: '0'
|
203
|
+
requirements: []
|
204
|
+
rubyforge_project:
|
205
|
+
rubygems_version: 2.7.8
|
206
|
+
signing_key:
|
207
|
+
specification_version: 4
|
208
|
+
summary: 'Stop using views: frontend components architecture for Ruby on Rails.'
|
209
|
+
test_files: []
|