From 2cab1388796c8427571a72f0061e9a549282471a Mon Sep 17 00:00:00 2001 From: Vladimir Dementyev Date: Sun, 26 Nov 2023 08:57:43 +0100 Subject: [PATCH] feat: bool style variants --- CHANGELOG.md | 4 + README.md | 34 ++++- lib/view_component_contrib/style_variants.rb | 31 ++++- test/cases/style_variants_test.rb | 139 +++++++++++-------- 4 files changed, 144 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c085d0a..748f8ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## master +- Add `compound` styles support. ([@palkan][]) + +- Support using booleans as style variant values. ([@palkan][]) + ## 0.2.1 (2023-11-16) - Fix style variants inhertiance. ([@palkan][]) diff --git a/README.md b/README.md index 54c0db5..df644c3 100644 --- a/README.md +++ b/README.md @@ -209,15 +209,19 @@ class ButtonComponent < ViewComponent::Base md { "text-base" } lg { "px-4 py-3 text-lg" } } + disabled { + yes { "opacity-75" } + } } defaults { {size: :md, color: :primary} } end - attr_reader :size, :color + attr_reader :size, :color, :disabled - def initialize(size: nil, color: nil) + def initialize(size: nil, color: nil, disabled: false) @size = size @color = color + @disabled = disabled end end ``` @@ -234,6 +238,12 @@ Passing `size: :lg` and `color: :secondary` would result in the following HTML: ``` +The `true` / `false` variant value would be converted into the `yes` / `no` variants: + +```erb + +``` + **NOTE:** If you pass `nil`, the default value would be used. You can define multiple style sets in a single component: @@ -308,6 +318,26 @@ end The specified variants are passed as block arguments, so you can implement dynamic styling. +If you prefer declarative approach, you can use the special `compound` directive. The previous example could be rewritten as follows: + +```ruby +style do + variants { + size { + sm { "text-sm" } + md { "text-base" } + lg { "px-4 py-3 text-lg" } + } + theme { + primary { %w[bg-blue-500 text-white] } + secondary { %w[bg-purple-500 text-white] } + } + } + + compound(size: :lg, theme: :primary) { %w[uppercase] } +end +``` + ### Using with TailwindCSS LSP To make completions (and other LSP features) work with our DSL, try the following configuration: diff --git a/lib/view_component_contrib/style_variants.rb b/lib/view_component_contrib/style_variants.rb index e559b1c..9013bd0 100644 --- a/lib/view_component_contrib/style_variants.rb +++ b/lib/view_component_contrib/style_variants.rb @@ -75,6 +75,7 @@ def initialize(&init_block) @base_block = nil @defaults = {} @variants = {} + @compounds = {} instance_eval(&init_block) if init_block end @@ -91,12 +92,26 @@ def variants(&block) @variants = VariantBuilder.new(true).build(&block) end + def compound(**variants, &block) + @compounds[variants] = block + end + def compile(**variants) acc = Array(@base_block&.call || []) - @defaults.merge(variants.compact).each do |variant, value| + config = @defaults.merge(variants.compact) + + config.each do |variant, value| + value = cast_value(value) variant = @variants.dig(variant, value) || next - styles = variant.is_a?(::Proc) ? variant.call(**variants) : variant + styles = variant.is_a?(::Proc) ? variant.call(**config) : variant + acc.concat(Array(styles)) + end + + @compounds.each do |compound, value| + next unless compound.all? { |k, v| config[k] == v } + + styles = value.is_a?(::Proc) ? value.call(**config) : value acc.concat(Array(styles)) end @@ -107,8 +122,20 @@ def dup copy = super copy.instance_variable_set(:@defaults, @defaults.dup) copy.instance_variable_set(:@variants, @variants.dup) + copy.instance_variable_set(:@compounds, @compounds.dup) copy end + + private + + def cast_value(val) + case val + when true then :yes + when false then :no + else + val + end + end end class StyleConfig # :nodoc: diff --git a/test/cases/style_variants_test.rb b/test/cases/style_variants_test.rb index c97340a..46b65bc 100644 --- a/test/cases/style_variants_test.rb +++ b/test/cases/style_variants_test.rb @@ -7,7 +7,7 @@ class Component < ViewComponentContrib::Base include ViewComponentContrib::StyleVariants erb_template <<~ERB -
Hello
+
Hello
ERB style do @@ -23,19 +23,45 @@ class Component < ViewComponentContrib::Base md { %w[text-md] } lg { %w[text-lg] } } + disabled { + yes { "opacity-50" } + } } defaults { {theme: :primary, size: :sm} } end - attr_reader :theme, :size + attr_reader :theme, :size, :disabled - def initialize(theme: :primary, size: :md) + def initialize(theme: :primary, size: :md, disabled: false) @theme = theme @size = size + @disabled = disabled end end + def test_render_variants + component = Component.new + + render_inline(component) + + assert_css "div.flex.flex-col.primary-color.primary-bg.text-md" + + component = Component.new(theme: :secondary, size: :md, disabled: true) + + render_inline(component) + + assert_css "div.secondary-color.secondary-bg.text-md.opacity-50" + end + + def test_render_defaults + component = Component.new(theme: nil, size: nil) + + render_inline(component) + + assert_css "div.flex.flex-col.primary-color.primary-bg.text-sm" + end + class SubComponent < Component erb_template <<~ERB
@@ -63,6 +89,22 @@ def initialize(mode: :light, **parent_opts) end end + def test_inheritance + component = SubComponent.new(theme: :secondary, size: :lg, mode: :dark) + + render_inline(component) + + assert_css "div.secondary-color.secondary-bg.text-lg" + + assert_css "a.text-white" + + component = SubComponent.new(mode: :light) + + render_inline(component) + + assert_css "a.text-black" + end + class PostProccesedComponent < Component style_config.postprocess_with do |compiled| compiled.join(" ").gsub("primary", "karamba") @@ -73,6 +115,14 @@ class PostProccesedComponent < Component ERB end + def test_postprocessor + component = PostProccesedComponent.new + + render_inline(component) + + assert_css "div.karamba-color.karamba-bg.text-md" + end + class DiffStyleSubcomponent < Component erb_template <<~ERB
Hello
@@ -94,6 +144,20 @@ class DiffStyleSubcomponent < Component end end + def test_style_config_inheritance + component = SubComponent.new(theme: :secondary, size: :lg, mode: :dark) + + render_inline(component) + + assert_css "a.text-white" + + component = DiffStyleSubcomponent.new + + render_inline(component) + + assert_css "div.text-white.font-md" + end + class CompoundComponent < Component style do variants { @@ -112,80 +176,35 @@ class CompoundComponent < Component secondary { %w[secondary-color secondary-bg] } } } - end - end - - def test_render_variants - component = Component.new - - render_inline(component) - - assert_css "div.flex.flex-col.primary-color.primary-bg.text-md" - - component = Component.new(theme: :secondary, size: :md) - - render_inline(component) - - assert_css "div.secondary-color.secondary-bg.text-md" - end - - def test_render_defaults - component = Component.new(theme: nil, size: nil) - - render_inline(component) - assert_css "div.flex.flex-col.primary-color.primary-bg.text-sm" - end - - def test_inheritance - component = SubComponent.new(theme: :secondary, size: :lg, mode: :dark) - - render_inline(component) - - assert_css "div.secondary-color.secondary-bg.text-lg" - - assert_css "a.text-white" - - component = SubComponent.new(mode: :light) - - render_inline(component) - - assert_css "a.text-black" - end - - def test_postprocessor - component = PostProccesedComponent.new - - render_inline(component) - - assert_css "div.karamba-color.karamba-bg.text-md" + compound(size: :sm, theme: :primary) { %w[rounded] } + compound(size: :md, theme: :secondary) { "underline" } + end end - def test_style_config_inheritance - component = SubComponent.new(theme: :secondary, size: :lg, mode: :dark) + def test_dynamic_variants + component = CompoundComponent.new(theme: :primary, size: :md) render_inline(component) - assert_css "a.text-white" + assert_css "div.primary-color.primary-bg.text-md" - component = DiffStyleSubcomponent.new + component = CompoundComponent.new(theme: :primary, size: :lg) render_inline(component) - assert_css "div.text-white.font-md" - end + assert_css "div.primary-color.primary-bg.text-lg.uppercase" - def test_dynamic_variants - component = CompoundComponent.new + component = CompoundComponent.new(theme: :primary, size: :sm) render_inline(component) - assert_css "div.primary-color.primary-bg.text-md" + assert_css "div.primary-color.primary-bg.text-sm.rounded" - component = CompoundComponent.new(theme: :primary, size: :lg) + component = CompoundComponent.new(theme: :secondary, size: :md) render_inline(component) - assert_css "div.primary-color.primary-bg.text-lg.uppercase" + assert_css "div.secondary-color.secondary-bg.text-md.underline" end end