view_component-contrib 0.1.6 → 0.2.0
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/CHANGELOG.md +10 -0
- data/README.md +171 -21
- data/lib/view_component_contrib/style_variants.rb +171 -0
- data/lib/view_component_contrib/version.rb +1 -1
- data/lib/view_component_contrib.rb +1 -0
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 814046bee78131bac6659eaa62160b6dfef169d48d1c5850c03dbe6a00a2ad0f
|
4
|
+
data.tar.gz: ef1ec7cfb3f71bf4db995be3a564dcfc0ff04e72d3c4e116515d216de18b38e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a5d578dfc44e7318da343a0aefe65946cce2399675d92646fd33db8e7494040d16433a3c409a08185d04c249fbc538e494175dce593a6bde19e27d254926d42
|
7
|
+
data.tar.gz: d01e0bf7f11d866aaab42dafb040e7ee45e4257524888b7bb6768b399d2db03c971867e6b14d839c1a66f7e37d9fe71982e287c8280b3a16811891bc61ee4723
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.2.0 (2023-11-07)
|
6
|
+
|
7
|
+
- Introduce style variants. ([@palkan][])
|
8
|
+
|
9
|
+
- **Require Ruby 2.7+**. ([@palkan][])
|
10
|
+
|
11
|
+
- Add system tests to generator. ([@palkan][])
|
12
|
+
|
13
|
+
- Drop Webpack-related stuff from the generator. ([@palkan][])
|
14
|
+
|
5
15
|
## 0.1.6 (2023-11-07)
|
6
16
|
|
7
17
|
- Support preview classes named `<component|partial>_preview.rb`. ([@palkan][])
|
data/README.md
CHANGED
@@ -32,7 +32,6 @@ The command above:
|
|
32
32
|
- Configure `view_component` paths.
|
33
33
|
- Adds `ApplicationViewComponent` and `ApplicationViewComponentPreview` classes.
|
34
34
|
- Configures testing framework (RSpec or Minitest).
|
35
|
-
- Adds required JS/CSS configuration.
|
36
35
|
- **Adds a custom generator to create components**.
|
37
36
|
|
38
37
|
The custom generator would allow you to create all the required component files in a single command:
|
@@ -94,6 +93,8 @@ ActiveSupport.on_load(:view_component) do
|
|
94
93
|
end
|
95
94
|
```
|
96
95
|
|
96
|
+
You can still continue using preview clases with the `_preview.rb` suffix, they would work as before.
|
97
|
+
|
97
98
|
#### Reducing previews boilerplate
|
98
99
|
|
99
100
|
In most cases, previews contain only the `default` example and a very simple template (`= render Component.new(**options)`).
|
@@ -180,9 +181,109 @@ end
|
|
180
181
|
|
181
182
|
If you need more control over your template, you can add a custom `preview.html.*` template (which will be used for all examples in this preview), or even create an example-specific `previews/example.html.*` (e.g. `previews/mobile.html.erb`).
|
182
183
|
|
184
|
+
## Style variants
|
185
|
+
|
186
|
+
Since v0.2.0, we provide a custom extentions to manage CSS classes and their combinations—**Style Variants**. This is especially useful for project using CSS frameworks such as TailwindCSS.
|
187
|
+
|
188
|
+
The idea is to define variants schema in the component class and use it to compile the resulting list of CSS classes. (Inspired by [Tailwind Variants](https://www.tailwind-variants.org) and [CVA variants](https://cva.style/docs/getting-started/variants)).
|
189
|
+
|
190
|
+
Consider an example:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
class ButtonComponent < ViewComponent::Base
|
194
|
+
include ViewComponentContrib::StyleVariants
|
195
|
+
|
196
|
+
style do
|
197
|
+
base {
|
198
|
+
%w[
|
199
|
+
font-medium bg-blue-500 text-white rounded-full
|
200
|
+
]
|
201
|
+
}
|
202
|
+
variants {
|
203
|
+
color {
|
204
|
+
primary { %w[bg-blue-500 text-white] }
|
205
|
+
secondary { %w[bg-purple-500 text-white] }
|
206
|
+
}
|
207
|
+
size {
|
208
|
+
sm { "text-sm" }
|
209
|
+
md { "text-base" }
|
210
|
+
lg { "px-4 py-3 text-lg" }
|
211
|
+
}
|
212
|
+
}
|
213
|
+
defaults { {size: :md, color: :primary} }
|
214
|
+
end
|
215
|
+
|
216
|
+
attr_reader :size, :color
|
217
|
+
|
218
|
+
def initialize(size: nil, color: nil)
|
219
|
+
@size = size
|
220
|
+
@color = color
|
221
|
+
end
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
Now, in the template, you can use the `#style` method and pass the variants to it:
|
226
|
+
|
227
|
+
```erb
|
228
|
+
<button class="<%= style(size:, color:) %>">Click me</button>
|
229
|
+
```
|
230
|
+
|
231
|
+
Passing `size: :lg` and `color: :secondary` would result in the following HTML:
|
232
|
+
|
233
|
+
```html
|
234
|
+
<button class="font-medium bg-purple-500 text-white rounded-full px-4 py-3 text-lg">Click me</button>
|
235
|
+
```
|
236
|
+
|
237
|
+
**NOTE:** If you pass `nil`, the default value would be used.
|
238
|
+
|
239
|
+
You can define multiple style sets in a single component:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
class ButtonComponent < ViewComponent::Base
|
243
|
+
include ViewComponentContrib::StyleVariants
|
244
|
+
|
245
|
+
# default component styles
|
246
|
+
style do
|
247
|
+
# ...
|
248
|
+
end
|
249
|
+
|
250
|
+
style :image do
|
251
|
+
variants {
|
252
|
+
orient {
|
253
|
+
portrait { "w-32 h-32" }
|
254
|
+
landscape { "w-64 h-32" }
|
255
|
+
}
|
256
|
+
}
|
257
|
+
end
|
258
|
+
end
|
259
|
+
```
|
260
|
+
|
261
|
+
And in the template:
|
262
|
+
|
263
|
+
```erb
|
264
|
+
<div>
|
265
|
+
<button class="<%= style(size:, theme:) %>">Click me</button>
|
266
|
+
<img src="..." class="<%= style(:image, orient: :portrait) %>">
|
267
|
+
</div>
|
268
|
+
```
|
269
|
+
|
270
|
+
Finally, you can inject into the class list compilation process to add your own logic:
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
class ButtonComponent < ViewComponent::Base
|
274
|
+
include ViewComponentContrib::StyleVariants
|
275
|
+
|
276
|
+
# You can provide either a proc or any other callable object
|
277
|
+
style_config.postprocess_with do |classes|
|
278
|
+
# classes is an array of CSS classes
|
279
|
+
TailwindMerge.call(classes).join(" ")
|
280
|
+
end
|
281
|
+
end
|
282
|
+
```
|
283
|
+
|
183
284
|
## Organizing assets (JS, CSS)
|
184
285
|
|
185
|
-
**NOTE
|
286
|
+
**NOTE**: This section assumes the usage of Vite or Webpack. See [this discussion](https://github.com/palkan/view_component-contrib/discussions/14) for other options.
|
186
287
|
|
187
288
|
We store JS and CSS files in the same sidecar folder:
|
188
289
|
|
@@ -203,6 +304,18 @@ import "./index.css"
|
|
203
304
|
|
204
305
|
In the root of the `components` folder we have the `index.js` file, which loads all the components:
|
205
306
|
|
307
|
+
- With Vite:
|
308
|
+
|
309
|
+
```js
|
310
|
+
// With Vite
|
311
|
+
import.meta.glob("./**/index.js").forEach((path) => {
|
312
|
+
const mod = await import(path);
|
313
|
+
mod.default();
|
314
|
+
});
|
315
|
+
```
|
316
|
+
|
317
|
+
- With Webpack:
|
318
|
+
|
206
319
|
```js
|
207
320
|
// components/index.js
|
208
321
|
const context = require.context(".", true, /index.js$/)
|
@@ -211,12 +324,11 @@ context.keys().forEach(context);
|
|
211
324
|
|
212
325
|
### Using with StimulusJS
|
213
326
|
|
214
|
-
You can define Stimulus controllers right in the `
|
327
|
+
You can define Stimulus controllers right in the component folder in the `controller.js` file:
|
215
328
|
|
216
329
|
```js
|
217
|
-
import "./index.css"
|
218
330
|
// We reserve Controller for the export name
|
219
|
-
import { Controller as BaseController } from "stimulus";
|
331
|
+
import { Controller as BaseController } from "@hotwired/stimulus";
|
220
332
|
|
221
333
|
export class Controller extends BaseController {
|
222
334
|
connect() {
|
@@ -225,20 +337,60 @@ export class Controller extends BaseController {
|
|
225
337
|
}
|
226
338
|
```
|
227
339
|
|
228
|
-
Then,
|
340
|
+
Then, in your Stimulus entrypoint, you can load and register your component controllers as follows:
|
341
|
+
|
342
|
+
- With Vite:
|
229
343
|
|
230
344
|
```js
|
231
|
-
|
232
|
-
// module, so you can use it for non-component controllers
|
345
|
+
import { Application } from "@hotwired/stimulus";
|
233
346
|
|
234
|
-
|
347
|
+
const application = Application.start();
|
348
|
+
|
349
|
+
// Configure Stimulus development experience
|
350
|
+
application.debug = false;
|
351
|
+
window.Stimulus = application;
|
352
|
+
|
353
|
+
// Generic controllers
|
354
|
+
const genericControllers = import.meta.globEager(
|
355
|
+
"../controllers/**/*_controller.js"
|
356
|
+
);
|
357
|
+
|
358
|
+
for (let path in genericControllers) {
|
359
|
+
let module = genericControllers[path];
|
360
|
+
let name = path
|
361
|
+
.match(/controllers\/(.+)_controller\.js$/)[1]
|
362
|
+
.replaceAll("/", "-")
|
363
|
+
.replaceAll("_", "-");
|
364
|
+
|
365
|
+
application.register(name, module.default);
|
366
|
+
}
|
367
|
+
|
368
|
+
// Controllers from components
|
369
|
+
const controllers = import.meta.globEager(
|
370
|
+
"./../../app/frontend/components/**/controller.js"
|
371
|
+
);
|
372
|
+
|
373
|
+
for (let path in controllers) {
|
374
|
+
let module = controllers[path];
|
375
|
+
let name = path
|
376
|
+
.match(/app\/frontend\/components\/(.+)\/controller\.js$/)[1]
|
377
|
+
.replaceAll("/", "-")
|
378
|
+
.replaceAll("_", "-");
|
379
|
+
application.register(name, module.default);
|
380
|
+
}
|
381
|
+
|
382
|
+
export default application;
|
383
|
+
```
|
384
|
+
|
385
|
+
- With Webpack:
|
386
|
+
|
387
|
+
```js
|
235
388
|
import { Application } from "stimulus";
|
236
389
|
export const application = Application.start();
|
237
390
|
|
238
|
-
//
|
239
|
-
import { application } from "../init/stimulus";
|
391
|
+
// ... other controllers
|
240
392
|
|
241
|
-
const context = require.context("
|
393
|
+
const context = require.context("./../../app/frontend/components/", true, /controllers.js$/)
|
242
394
|
context.keys().forEach((path) => {
|
243
395
|
const mod = context(path);
|
244
396
|
|
@@ -248,9 +400,10 @@ context.keys().forEach((path) => {
|
|
248
400
|
// Convert path into a controller identifier:
|
249
401
|
// example/index.js -> example
|
250
402
|
// nav/user_info/index.js -> nav--user-info
|
251
|
-
const identifier = path
|
252
|
-
.
|
253
|
-
.
|
403
|
+
const identifier = path
|
404
|
+
.match(/app\/frontend\/components\/(.+)\/controller\.js$/)[1]
|
405
|
+
.replaceAll("/", "-")
|
406
|
+
.replaceAll("_", "-");
|
254
407
|
|
255
408
|
application.register(identifier, mod.Controller);
|
256
409
|
});
|
@@ -265,6 +418,8 @@ class ApplicationViewComponent
|
|
265
418
|
def identifier
|
266
419
|
@identifier ||= self.class.name.sub("::Component", "").underscore.split("/").join("--")
|
267
420
|
end
|
421
|
+
|
422
|
+
alias_method :controller_name, :identifier
|
268
423
|
end
|
269
424
|
```
|
270
425
|
|
@@ -272,7 +427,7 @@ And now in your template:
|
|
272
427
|
|
273
428
|
```erb
|
274
429
|
<!-- component.html -->
|
275
|
-
<div data-controller="<%=
|
430
|
+
<div data-controller="<%= controller_name %>">
|
276
431
|
</div>
|
277
432
|
```
|
278
433
|
|
@@ -495,11 +650,6 @@ And the template looks like this now:
|
|
495
650
|
|
496
651
|
You can use the `#wrapped` method on any component inherited from `ApplicationViewComponent` to wrap it automatically:
|
497
652
|
|
498
|
-
## ToDo list
|
499
|
-
|
500
|
-
- Better preview tools (w/o JS deps 😉).
|
501
|
-
- Hotwire-related extensions.
|
502
|
-
|
503
653
|
## License
|
504
654
|
|
505
655
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponentContrib
|
4
|
+
# Organize style in variants that can be combined.
|
5
|
+
# Inspired by https://www.tailwind-variants.org and https://cva.style/docs/getting-started/variants
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
#
|
9
|
+
# class ButtonComponent < ViewComponent::Base
|
10
|
+
# include ViewComponentContrib::StyleVariants
|
11
|
+
#
|
12
|
+
# erb_template <<~ERB
|
13
|
+
# <button class="<%= style(size: 'sm', color: 'secondary') %>">Click me</button>
|
14
|
+
# ERB
|
15
|
+
#
|
16
|
+
# style do
|
17
|
+
# base {
|
18
|
+
# %w(
|
19
|
+
# font-medium bg-blue-500 text-white rounded-full
|
20
|
+
# )
|
21
|
+
# }
|
22
|
+
# variants {
|
23
|
+
# color {
|
24
|
+
# primary { %w(bg-blue-500 text-white) }
|
25
|
+
# secondary { %w(bg-purple-500 text-white) }
|
26
|
+
# }
|
27
|
+
# size {
|
28
|
+
# sm { "text-sm" }
|
29
|
+
# md { "text-base" }
|
30
|
+
# lg { "px-4 py-3 text-lg" }
|
31
|
+
# }
|
32
|
+
# }
|
33
|
+
# defaults { {size: :md, color: :primary} }
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# attr_reader :size, :color
|
37
|
+
#
|
38
|
+
# def initialize(size: :md, color: :primary)
|
39
|
+
# @size = size
|
40
|
+
# @color = color
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
module StyleVariants
|
45
|
+
class VariantBuilder
|
46
|
+
attr_reader :unwrap_blocks
|
47
|
+
|
48
|
+
def initialize(unwrap_blocks = true)
|
49
|
+
@unwrap_blocks = unwrap_blocks
|
50
|
+
@variants = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def build(&block)
|
54
|
+
instance_eval(&block)
|
55
|
+
@variants
|
56
|
+
end
|
57
|
+
|
58
|
+
def respond_to_missing?(name, include_private = false)
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
def method_missing(name, &block)
|
63
|
+
return super unless block_given?
|
64
|
+
|
65
|
+
@variants[name] = if unwrap_blocks
|
66
|
+
VariantBuilder.new(false).build(&block)
|
67
|
+
else
|
68
|
+
block
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class StyleSet
|
74
|
+
def initialize(&init_block)
|
75
|
+
@base_block = nil
|
76
|
+
@defaults = {}
|
77
|
+
@variants = {}
|
78
|
+
|
79
|
+
instance_eval(&init_block) if init_block
|
80
|
+
end
|
81
|
+
|
82
|
+
def base(&block)
|
83
|
+
@base_block = block
|
84
|
+
end
|
85
|
+
|
86
|
+
def defaults(&block)
|
87
|
+
@defaults = block.call.freeze
|
88
|
+
end
|
89
|
+
|
90
|
+
def variants(&block)
|
91
|
+
@variants = VariantBuilder.new(true).build(&block)
|
92
|
+
end
|
93
|
+
|
94
|
+
def compile(**variants)
|
95
|
+
acc = Array(@base_block&.call || [])
|
96
|
+
|
97
|
+
@defaults.merge(variants.compact).each do |variant, value|
|
98
|
+
variant = @variants.dig(variant, value) || next
|
99
|
+
styles = variant.is_a?(::Proc) ? variant.call : variant
|
100
|
+
acc.concat(Array(styles))
|
101
|
+
end
|
102
|
+
|
103
|
+
acc
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class StyleConfig # :nodoc:
|
108
|
+
DEFAULT_POST_PROCESSOR = ->(compiled) { compiled.join(" ") }
|
109
|
+
|
110
|
+
attr_reader :postprocessor
|
111
|
+
|
112
|
+
def initialize
|
113
|
+
@styles = {}
|
114
|
+
@postprocessor = DEFAULT_POST_PROCESSOR
|
115
|
+
end
|
116
|
+
|
117
|
+
def define(name, &block)
|
118
|
+
styles[name] = StyleSet.new(&block)
|
119
|
+
end
|
120
|
+
|
121
|
+
def compile(name, **variants)
|
122
|
+
styles[name]&.compile(**variants).then do |compiled|
|
123
|
+
next unless compiled
|
124
|
+
|
125
|
+
postprocess(compiled)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Allow defining a custom postprocessor
|
130
|
+
def postprocess_with(callable = nil, &block)
|
131
|
+
@postprocessor = callable || block
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
attr_reader :styles
|
137
|
+
|
138
|
+
def postprocess(compiled) = postprocessor.call(compiled)
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.included(base)
|
142
|
+
base.extend ClassMethods
|
143
|
+
end
|
144
|
+
|
145
|
+
module ClassMethods
|
146
|
+
# Returns the name of the default style set based on the class name:
|
147
|
+
# MyComponent::Component => my_component
|
148
|
+
# Namespaced::MyComponent => my_component
|
149
|
+
def default_style_name
|
150
|
+
@default_style_name ||= name.demodulize.sub(/(::Component|Component)$/, "").underscore.presence || "component"
|
151
|
+
end
|
152
|
+
|
153
|
+
def style(name = default_style_name, &block)
|
154
|
+
style_config.define(name.to_sym, &block)
|
155
|
+
end
|
156
|
+
|
157
|
+
def style_config
|
158
|
+
@style_config ||=
|
159
|
+
if superclass.respond_to?(:style_config)
|
160
|
+
superclass.style_config.dup
|
161
|
+
else
|
162
|
+
StyleConfig.new
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def style(name = self.class.default_style_name, **variants)
|
168
|
+
self.class.style_config.compile(name.to_sym, **variants)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -11,6 +11,7 @@ module ViewComponentContrib
|
|
11
11
|
autoload :TranslationHelper, "view_component_contrib/translation_helper"
|
12
12
|
autoload :WrapperComponent, "view_component_contrib/wrapper_component"
|
13
13
|
autoload :WrappedHelper, "view_component_contrib/wrapped_helper"
|
14
|
+
autoload :StyleVariants, "view_component_contrib/style_variants"
|
14
15
|
|
15
16
|
autoload :Base, "view_component_contrib/base"
|
16
17
|
autoload :Preview, "view_component_contrib/preview"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: view_component-contrib
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-11-
|
11
|
+
date: 2023-11-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: view_component
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.
|
33
|
+
version: 0.15.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.
|
40
|
+
version: 0.15.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: bundler
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -156,6 +156,7 @@ files:
|
|
156
156
|
- lib/view_component_contrib/preview/default_template.rb
|
157
157
|
- lib/view_component_contrib/preview/sidecarable.rb
|
158
158
|
- lib/view_component_contrib/railtie.rb
|
159
|
+
- lib/view_component_contrib/style_variants.rb
|
159
160
|
- lib/view_component_contrib/translation_helper.rb
|
160
161
|
- lib/view_component_contrib/version.rb
|
161
162
|
- lib/view_component_contrib/wrapped_helper.rb
|
@@ -177,7 +178,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
177
178
|
requirements:
|
178
179
|
- - ">="
|
179
180
|
- !ruby/object:Gem::Version
|
180
|
-
version: '2.
|
181
|
+
version: '2.7'
|
181
182
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
182
183
|
requirements:
|
183
184
|
- - ">="
|