view_component-contrib 0.1.6 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="