view_component-contrib 0.2.2 → 0.2.4

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: eb40fc2b6095e17a5cb7402bb9c577ee9df9152b65342a3fd2879861b0ec61c4
4
- data.tar.gz: da852768b8dfe7907aae80bcee8afb5a52998185365e4f2bf702d2e5160df8cf
3
+ metadata.gz: 66619dee8f7cb3f26eddce9c5809ce7c879f8f464f2fa8ed140cd5a0ba08bfbd
4
+ data.tar.gz: e0f4013b8bebd93cab59f17ed52f1a6bd92c401b672919f49b386019296b0b1a
5
5
  SHA512:
6
- metadata.gz: d8492733e339fe678625730a079a5831c3647f9063aa13f50f3ef34d1a4c03a1505c536f06bb9cf3959310e311774b265c18cb9f01070d77b3d93a0e7b295241
7
- data.tar.gz: d6e0426d3014ea7978eaf807e9a8aa11b0d729f16309e303c2a6a2d0d2d09d34d9e2146293648b57aba7bece138c831f4182c983aea4c2602dfa3dcc769f0cf3
6
+ metadata.gz: 9d35a906a4c66e3097266b2b3f8df662fac38dd5f0ce4580e35d5e0a99cd3c90ee2ce6c5db36c3fc8ec16a9e9359fc600e61b205075d55ac36d853b77e6afb00
7
+ data.tar.gz: d66d56152567959e2f9cbe57ee0b9becaaeeed366179c88bfaf17b9bc1dd92b50295c996a03e169bfd397f9d2444d4ba8e3c9e7e7c48fe91cca49c5f658d00ca
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.2.4 (2025-01-03)
6
+
7
+ - Add inheritance strategies to style variants ([@omarluq][])
8
+
9
+ - Add special `class:` variant to `style` helper. For appending classes.
10
+ Inspired by https://cva.style/docs/getting-started/extending-components
11
+
12
+ ## 0.2.3 (2024-07-31)
13
+
14
+ - Fix publishing transpiled files (and bring Ruby 2.7 support back) ([@palkan][])
15
+
5
16
  ## 0.2.2 (2023-11-29)
6
17
 
7
18
  - Add `compound` styles support. ([@palkan][])
data/README.md CHANGED
@@ -277,6 +277,15 @@ And in the template:
277
277
  </div>
278
278
  ```
279
279
 
280
+ You can also add additional classes through thr `style` method using the special `class:` variant, like so:
281
+
282
+ ```erb
283
+ <div>
284
+ <button class="<%= style(size:, theme:, class: 'extra-class') %>">Click me</button>
285
+ <img src="..." class="<%= style(:image, orient: :portrait) %>">
286
+ </div>
287
+ ```
288
+
280
289
  Finally, you can inject into the class list compilation process to add your own logic:
281
290
 
282
291
  ```ruby
@@ -292,6 +301,69 @@ class ButtonComponent < ViewComponent::Base
292
301
  end
293
302
  ```
294
303
 
304
+ ### Style variants inheritance
305
+
306
+ Style variants support three inheritance strategies when extending components:
307
+
308
+ 1. `override` (default behavior): Completely replaces parent variants.
309
+ 2. `merge` (deep merge): Preserves all variant keys unless explicitly overwritten.
310
+ 3. `extend` (shallow merge): Preserves variants unless explicitly overwritten.
311
+
312
+ Consider an example:
313
+
314
+ ```ruby
315
+ class Parent::Component < ViewComponent::Base
316
+ include ViewComponentContrib::StyleVariants
317
+
318
+ style do
319
+ variants do
320
+ size {
321
+ md { "text-md" }
322
+ lg { "text-lg" }
323
+ }
324
+ disabled {
325
+ yes { "opacity-50" }
326
+ }
327
+ end
328
+ end
329
+ end
330
+
331
+ # Using override strategy (default)
332
+ class Child::Component < Parent::Component
333
+ style do
334
+ variants do
335
+ size {
336
+ lg { "text-larger" }
337
+ }
338
+ end
339
+ end
340
+ end
341
+
342
+ # Using merge strategy
343
+ class Child::Component < Parent::Component
344
+ style do
345
+ variants(strategy: :merge) do
346
+ size {
347
+ lg { "text-larger" }
348
+ }
349
+ end
350
+ end
351
+ end
352
+
353
+ # Using extend strategy
354
+ class Child::Component < Parent::Component
355
+ style do
356
+ variants(strategy: :extend) do
357
+ size {
358
+ lg { "text-larger" }
359
+ }
360
+ end
361
+ end
362
+ end
363
+ ```
364
+
365
+ In this example, the `override` strategy will only keep the `size.lg` variant, dropping all others. The `merge` strategy preserves all variants and their keys, only replacing the `size.lg` value. The `extend` strategy keeps all variants but replaces all keys of the overwritten `size` variant.
366
+
295
367
  ### Dependent (or compound) styles
296
368
 
297
369
  Sometimes it might be necessary to define complex styling rules, e.g., when a combination of variants requires adding additional styles. That's where usage of Ruby blocks for configuration becomes useful. For example:
@@ -0,0 +1,242 @@
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
+ @compounds = {}
79
+
80
+ return unless init_block
81
+
82
+ @init_block = init_block
83
+ instance_eval(&init_block)
84
+ end
85
+
86
+ def base(&block)
87
+ @base_block = block
88
+ end
89
+
90
+ def defaults(&block)
91
+ @defaults = block.call.freeze
92
+ end
93
+
94
+ def variants(strategy: :override, &block)
95
+ variants = build_variants(&block)
96
+ @variants = handle_variants(variants, strategy)
97
+ end
98
+
99
+ def build_variants(&block)
100
+ VariantBuilder.new(true).build(&block)
101
+ end
102
+
103
+ def handle_variants(variants, strategy)
104
+ return variants if strategy == :override
105
+
106
+ parent_variants = find_parent_variants
107
+ return variants unless parent_variants
108
+
109
+ return parent_variants.deep_merge(variants) if strategy == :merge
110
+
111
+ parent_variants.merge(variants) if strategy == :extend
112
+ end
113
+
114
+ def find_parent_variants
115
+ parent_component = @init_block.binding.receiver.superclass
116
+ return unless parent_component.respond_to?(:style_config)
117
+
118
+ parent_config = parent_component.style_config
119
+ default_parent_style = parent_component.default_style_name
120
+ parent_style_set = parent_config.instance_variable_get(:@styles)[default_parent_style.to_sym]
121
+ parent_style_set.instance_variable_get(:@variants).deep_dup
122
+ end
123
+
124
+ def compound(**variants, &block)
125
+ @compounds[variants] = block
126
+ end
127
+
128
+ def compile(**variants)
129
+ acc = Array(@base_block&.call || [])
130
+
131
+ config = @defaults.merge(variants.compact)
132
+
133
+ config.each do |variant, value|
134
+ value = cast_value(value)
135
+ variant = @variants.dig(variant, value) || next
136
+ styles = variant.is_a?(::Proc) ? variant.call(**config) : variant
137
+ acc.concat(Array(styles))
138
+ end
139
+
140
+ @compounds.each do |compound, value|
141
+ next unless compound.all? { |k, v| config[k] == v }
142
+
143
+ styles = value.is_a?(::Proc) ? value.call(**config) : value
144
+ acc.concat(Array(styles))
145
+ end
146
+
147
+ acc.concat(Array(config[:class]))
148
+ acc.concat(Array(config[:class_name]))
149
+ acc
150
+ end
151
+
152
+ def dup
153
+ copy = super
154
+ copy.instance_variable_set(:@defaults, @defaults.dup)
155
+ copy.instance_variable_set(:@variants, @variants.dup)
156
+ copy.instance_variable_set(:@compounds, @compounds.dup)
157
+ copy
158
+ end
159
+
160
+ private
161
+
162
+ def cast_value(val)
163
+ case val
164
+ when true then :yes
165
+ when false then :no
166
+ else
167
+ val
168
+ end
169
+ end
170
+ end
171
+
172
+ class StyleConfig # :nodoc:
173
+ DEFAULT_POST_PROCESSOR = ->(compiled) { compiled.join(" ") }
174
+
175
+ attr_reader :postprocessor
176
+
177
+ def initialize
178
+ @styles = {}
179
+ @postprocessor = DEFAULT_POST_PROCESSOR
180
+ end
181
+
182
+ def define(name, &block)
183
+ styles[name] = StyleSet.new(&block)
184
+ end
185
+
186
+ def compile(name, **variants)
187
+ styles[name]&.compile(**variants).then do |compiled|
188
+ next unless compiled
189
+
190
+ postprocess(compiled)
191
+ end
192
+ end
193
+
194
+ # Allow defining a custom postprocessor
195
+ def postprocess_with(callable = nil, &block)
196
+ @postprocessor = callable || block
197
+ end
198
+
199
+ def dup
200
+ copy = super
201
+ copy.instance_variable_set(:@styles, @styles.dup)
202
+ copy
203
+ end
204
+
205
+ private
206
+
207
+ attr_reader :styles
208
+
209
+ def postprocess(compiled) ; postprocessor.call(compiled); end
210
+ end
211
+
212
+ def self.included(base)
213
+ base.extend ClassMethods
214
+ end
215
+
216
+ module ClassMethods
217
+ # Returns the name of the default style set based on the class name:
218
+ # MyComponent::Component => my_component
219
+ # Namespaced::MyComponent => my_component
220
+ def default_style_name
221
+ @default_style_name ||= name.demodulize.sub(/(::Component|Component)$/, "").underscore.presence || "component"
222
+ end
223
+
224
+ def style(name = default_style_name, &block)
225
+ style_config.define(name.to_sym, &block)
226
+ end
227
+
228
+ def style_config
229
+ @style_config ||=
230
+ if superclass.respond_to?(:style_config)
231
+ superclass.style_config.dup
232
+ else
233
+ StyleConfig.new
234
+ end
235
+ end
236
+ end
237
+
238
+ def style(name = self.class.default_style_name, **variants)
239
+ self.class.style_config.compile(name.to_sym, **variants)
240
+ end
241
+ end
242
+ end
@@ -77,7 +77,10 @@ module ViewComponentContrib
77
77
  @variants = {}
78
78
  @compounds = {}
79
79
 
80
- instance_eval(&init_block) if init_block
80
+ return unless init_block
81
+
82
+ @init_block = init_block
83
+ instance_eval(&init_block)
81
84
  end
82
85
 
83
86
  def base(&block)
@@ -88,8 +91,34 @@ module ViewComponentContrib
88
91
  @defaults = block.call.freeze
89
92
  end
90
93
 
91
- def variants(&block)
92
- @variants = VariantBuilder.new(true).build(&block)
94
+ def variants(strategy: :override, &block)
95
+ variants = build_variants(&block)
96
+ @variants = handle_variants(variants, strategy)
97
+ end
98
+
99
+ def build_variants(&block)
100
+ VariantBuilder.new(true).build(&block)
101
+ end
102
+
103
+ def handle_variants(variants, strategy)
104
+ return variants if strategy == :override
105
+
106
+ parent_variants = find_parent_variants
107
+ return variants unless parent_variants
108
+
109
+ return parent_variants.deep_merge(variants) if strategy == :merge
110
+
111
+ parent_variants.merge(variants) if strategy == :extend
112
+ end
113
+
114
+ def find_parent_variants
115
+ parent_component = @init_block.binding.receiver.superclass
116
+ return unless parent_component.respond_to?(:style_config)
117
+
118
+ parent_config = parent_component.style_config
119
+ default_parent_style = parent_component.default_style_name
120
+ parent_style_set = parent_config.instance_variable_get(:@styles)[default_parent_style.to_sym]
121
+ parent_style_set.instance_variable_get(:@variants).deep_dup
93
122
  end
94
123
 
95
124
  def compound(**variants, &block)
@@ -115,6 +144,8 @@ module ViewComponentContrib
115
144
  acc.concat(Array(styles))
116
145
  end
117
146
 
147
+ acc.concat(Array(config[:class]))
148
+ acc.concat(Array(config[:class_name]))
118
149
  acc
119
150
  end
120
151
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ViewComponentContrib # :nodoc:all
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.4"
5
5
  end
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.2.2
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-29 00:00:00.000000000 Z
11
+ date: 2025-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: view_component
@@ -147,6 +147,7 @@ files:
147
147
  - LICENSE.txt
148
148
  - README.md
149
149
  - app/views/view_component_contrib/preview.html.erb
150
+ - lib/.rbnext/3.0/view_component_contrib/style_variants.rb
150
151
  - lib/view_component-contrib.rb
151
152
  - lib/view_component_contrib.rb
152
153
  - lib/view_component_contrib/base.rb
@@ -170,7 +171,7 @@ metadata:
170
171
  documentation_uri: http://github.com/palkan/view_component-contrib
171
172
  homepage_uri: http://github.com/palkan/view_component-contrib
172
173
  source_code_uri: http://github.com/palkan/view_component-contrib
173
- post_install_message:
174
+ post_install_message:
174
175
  rdoc_options: []
175
176
  require_paths:
176
177
  - lib
@@ -185,8 +186,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
186
  - !ruby/object:Gem::Version
186
187
  version: '0'
187
188
  requirements: []
188
- rubygems_version: 3.4.20
189
- signing_key:
189
+ rubygems_version: 3.4.19
190
+ signing_key:
190
191
  specification_version: 4
191
192
  summary: A collection of extensions and developer tools for ViewComponent
192
193
  test_files: []