view_component-contrib 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cebcea7442b79ea935dd0039c361e813b597c8f63a15a9cd90407635a6a96856
4
- data.tar.gz: 98e1aeecc1354a1e619bf75c765aa64f0051ac5ff4d7d0c81fb42425e7da7ae5
3
+ metadata.gz: b586c9ad03730cff1cc38721c6904f427c71a73f5777605f17fc0379f60f8dc7
4
+ data.tar.gz: 7c2ed9aed5c845492a8487fab7ce5e66c639d455d8b4027c383b047ad961fdf0
5
5
  SHA512:
6
- metadata.gz: 427691d12a38bfcab3c0861506aade1ada63422badb1b207e5fff49cd72bc66c8a6aadfd220b51385ef5abc2166061cd8333afbf14e915058e98bf2899438ad5
7
- data.tar.gz: 5eb44287d55acb724d458d6eca667296787215cafced750c9071834ee2203b89327d93ff983b1b2e9879b961e13eb8b5d4f2af237c690a4c3f6bc6cbdaa6654c
6
+ metadata.gz: c1d2f760734b6ca7f71da538fbba4c04b0ff91b885cc47e09f3943b81a5a097ef62293348dc3ac07e20a4b812e52cf8a4bd8b39b6cb306551dceac4a784c652b
7
+ data.tar.gz: baaf5f34009ca87228246f260357339849f3d22e988543f36c36e772293bede4f249741fee5749db46d37a78dd8dc94674e13181d618282463a31afd5dba48b7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.2.1 (2023-11-16)
6
+
7
+ - Fix style variants inhertiance. ([@palkan][])
8
+
9
+ ## 0.2.0 (2023-11-07)
10
+
11
+ - Introduce style variants. ([@palkan][])
12
+
13
+ - **Require Ruby 2.7+**. ([@palkan][])
14
+
15
+ - Add system tests to generator. ([@palkan][])
16
+
17
+ - Drop Webpack-related stuff from the generator. ([@palkan][])
18
+
5
19
  ## 0.1.6 (2023-11-07)
6
20
 
7
21
  - 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,125 @@ 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
+
284
+ ### Using with TailwindCSS LSP
285
+
286
+ To make completions (and other LSP features) work with our DSL, try the following configuration:
287
+
288
+ ```json
289
+ "tailwindCSS.includeLanguages": {
290
+ "erb": "html",
291
+ "ruby": "html"
292
+ },
293
+ "tailwindCSS.experimental.classRegex": [
294
+ "%w\\[([^\\]]*)\\]"
295
+ ]
296
+ ```
297
+
298
+ **NOTE:** It will only work with `%w[ ... ]` word arrays, but you can adjust it to your needs.
299
+
183
300
  ## Organizing assets (JS, CSS)
184
301
 
185
- **NOTE*: This section assumes the usage of Webpack, Vite or other _frontend_ builder (e.g., not Sprockets).
302
+ **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
303
 
187
304
  We store JS and CSS files in the same sidecar folder:
188
305
 
@@ -203,6 +320,18 @@ import "./index.css"
203
320
 
204
321
  In the root of the `components` folder we have the `index.js` file, which loads all the components:
205
322
 
323
+ - With Vite:
324
+
325
+ ```js
326
+ // With Vite
327
+ import.meta.glob("./**/index.js").forEach((path) => {
328
+ const mod = await import(path);
329
+ mod.default();
330
+ });
331
+ ```
332
+
333
+ - With Webpack:
334
+
206
335
  ```js
207
336
  // components/index.js
208
337
  const context = require.context(".", true, /index.js$/)
@@ -211,12 +340,11 @@ context.keys().forEach(context);
211
340
 
212
341
  ### Using with StimulusJS
213
342
 
214
- You can define Stimulus controllers right in the `index.js` file using the following approach:
343
+ You can define Stimulus controllers right in the component folder in the `controller.js` file:
215
344
 
216
345
  ```js
217
- import "./index.css"
218
346
  // We reserve Controller for the export name
219
- import { Controller as BaseController } from "stimulus";
347
+ import { Controller as BaseController } from "@hotwired/stimulus";
220
348
 
221
349
  export class Controller extends BaseController {
222
350
  connect() {
@@ -225,20 +353,60 @@ export class Controller extends BaseController {
225
353
  }
226
354
  ```
227
355
 
228
- Then, we need to update the `components/index.js` to automatically register controllers:
356
+ Then, in your Stimulus entrypoint, you can load and register your component controllers as follows:
357
+
358
+ - With Vite:
229
359
 
230
360
  ```js
231
- // We recommend putting Stimulus application instance into its own
232
- // module, so you can use it for non-component controllers
361
+ import { Application } from "@hotwired/stimulus";
362
+
363
+ const application = Application.start();
364
+
365
+ // Configure Stimulus development experience
366
+ application.debug = false;
367
+ window.Stimulus = application;
368
+
369
+ // Generic controllers
370
+ const genericControllers = import.meta.globEager(
371
+ "../controllers/**/*_controller.js"
372
+ );
373
+
374
+ for (let path in genericControllers) {
375
+ let module = genericControllers[path];
376
+ let name = path
377
+ .match(/controllers\/(.+)_controller\.js$/)[1]
378
+ .replaceAll("/", "-")
379
+ .replaceAll("_", "-");
380
+
381
+ application.register(name, module.default);
382
+ }
383
+
384
+ // Controllers from components
385
+ const controllers = import.meta.globEager(
386
+ "./../../app/frontend/components/**/controller.js"
387
+ );
388
+
389
+ for (let path in controllers) {
390
+ let module = controllers[path];
391
+ let name = path
392
+ .match(/app\/frontend\/components\/(.+)\/controller\.js$/)[1]
393
+ .replaceAll("/", "-")
394
+ .replaceAll("_", "-");
395
+ application.register(name, module.default);
396
+ }
233
397
 
234
- // init/stimulus.js
398
+ export default application;
399
+ ```
400
+
401
+ - With Webpack:
402
+
403
+ ```js
235
404
  import { Application } from "stimulus";
236
405
  export const application = Application.start();
237
406
 
238
- // components/index.js
239
- import { application } from "../init/stimulus";
407
+ // ... other controllers
240
408
 
241
- const context = require.context(".", true, /index.js$/)
409
+ const context = require.context("./../../app/frontend/components/", true, /controllers.js$/)
242
410
  context.keys().forEach((path) => {
243
411
  const mod = context(path);
244
412
 
@@ -248,9 +416,10 @@ context.keys().forEach((path) => {
248
416
  // Convert path into a controller identifier:
249
417
  // example/index.js -> example
250
418
  // nav/user_info/index.js -> nav--user-info
251
- const identifier = path.replace(/^\.\//, '')
252
- .replace(/\/index\.js$/, '')
253
- .replace(/\//g, '--');
419
+ const identifier = path
420
+ .match(/app\/frontend\/components\/(.+)\/controller\.js$/)[1]
421
+ .replaceAll("/", "-")
422
+ .replaceAll("_", "-");
254
423
 
255
424
  application.register(identifier, mod.Controller);
256
425
  });
@@ -265,6 +434,8 @@ class ApplicationViewComponent
265
434
  def identifier
266
435
  @identifier ||= self.class.name.sub("::Component", "").underscore.split("/").join("--")
267
436
  end
437
+
438
+ alias_method :controller_name, :identifier
268
439
  end
269
440
  ```
270
441
 
@@ -272,7 +443,7 @@ And now in your template:
272
443
 
273
444
  ```erb
274
445
  <!-- component.html -->
275
- <div data-controller="<%= identifier %>">
446
+ <div data-controller="<%= controller_name %>">
276
447
  </div>
277
448
  ```
278
449
 
@@ -495,11 +666,6 @@ And the template looks like this now:
495
666
 
496
667
  You can use the `#wrapped` method on any component inherited from `ApplicationViewComponent` to wrap it automatically:
497
668
 
498
- ## ToDo list
499
-
500
- - Better preview tools (w/o JS deps 😉).
501
- - Hotwire-related extensions.
502
-
503
669
  ## License
504
670
 
505
671
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,184 @@
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
+
106
+ def dup
107
+ copy = super
108
+ copy.instance_variable_set(:@defaults, @defaults.dup)
109
+ copy.instance_variable_set(:@variants, @variants.dup)
110
+ copy
111
+ end
112
+ end
113
+
114
+ class StyleConfig # :nodoc:
115
+ DEFAULT_POST_PROCESSOR = ->(compiled) { compiled.join(" ") }
116
+
117
+ attr_reader :postprocessor
118
+
119
+ def initialize
120
+ @styles = {}
121
+ @postprocessor = DEFAULT_POST_PROCESSOR
122
+ end
123
+
124
+ def define(name, &block)
125
+ styles[name] = StyleSet.new(&block)
126
+ end
127
+
128
+ def compile(name, **variants)
129
+ styles[name]&.compile(**variants).then do |compiled|
130
+ next unless compiled
131
+
132
+ postprocess(compiled)
133
+ end
134
+ end
135
+
136
+ # Allow defining a custom postprocessor
137
+ def postprocess_with(callable = nil, &block)
138
+ @postprocessor = callable || block
139
+ end
140
+
141
+ def dup
142
+ copy = super
143
+ copy.instance_variable_set(:@styles, @styles.dup)
144
+ copy
145
+ end
146
+
147
+ private
148
+
149
+ attr_reader :styles
150
+
151
+ def postprocess(compiled) = postprocessor.call(compiled)
152
+ end
153
+
154
+ def self.included(base)
155
+ base.extend ClassMethods
156
+ end
157
+
158
+ module ClassMethods
159
+ # Returns the name of the default style set based on the class name:
160
+ # MyComponent::Component => my_component
161
+ # Namespaced::MyComponent => my_component
162
+ def default_style_name
163
+ @default_style_name ||= name.demodulize.sub(/(::Component|Component)$/, "").underscore.presence || "component"
164
+ end
165
+
166
+ def style(name = default_style_name, &block)
167
+ style_config.define(name.to_sym, &block)
168
+ end
169
+
170
+ def style_config
171
+ @style_config ||=
172
+ if superclass.respond_to?(:style_config)
173
+ superclass.style_config.dup
174
+ else
175
+ StyleConfig.new
176
+ end
177
+ end
178
+ end
179
+
180
+ def style(name = self.class.default_style_name, **variants)
181
+ self.class.style_config.compile(name.to_sym, **variants)
182
+ end
183
+ end
184
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ViewComponentContrib # :nodoc:all
4
- VERSION = "0.1.6"
4
+ VERSION = "0.2.1"
5
5
  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.1.6
4
+ version: 0.2.1
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-07 00:00:00.000000000 Z
11
+ date: 2023-11-16 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.12.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.12.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.6'
181
+ version: '2.7'
181
182
  required_rubygems_version: !ruby/object:Gem::Requirement
182
183
  requirements:
183
184
  - - ">="