view_component-contrib 0.2.2 → 0.2.4

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: 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: []