Skip to content

Commit

Permalink
feat: bool style variants
Browse files Browse the repository at this point in the history
  • Loading branch information
palkan committed Nov 27, 2023
1 parent 25d0aa0 commit 2cab138
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 64 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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][])
Expand Down
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -234,6 +238,12 @@ Passing `size: :lg` and `color: :secondary` would result in the following HTML:
<button class="font-medium bg-purple-500 text-white rounded-full px-4 py-3 text-lg">Click me</button>
```

The `true` / `false` variant value would be converted into the `yes` / `no` variants:

```erb
<button class="<%= style(size:, color:, disabled: true) %>">Click me</button>
```

**NOTE:** If you pass `nil`, the default value would be used.

You can define multiple style sets in a single component:
Expand Down Expand Up @@ -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:
Expand Down
31 changes: 29 additions & 2 deletions lib/view_component_contrib/style_variants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def initialize(&init_block)
@base_block = nil
@defaults = {}
@variants = {}
@compounds = {}

instance_eval(&init_block) if init_block
end
Expand All @@ -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

Expand All @@ -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:
Expand Down
139 changes: 79 additions & 60 deletions test/cases/style_variants_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Component < ViewComponentContrib::Base
include ViewComponentContrib::StyleVariants

erb_template <<~ERB
<div class="<%= style(theme: theme, size: size) %>">Hello</div>
<div class="<%= style(theme: theme, size: size, disabled: disabled) %>">Hello</div>
ERB

style do
Expand All @@ -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
<div class="<%= style(:component, theme: theme, size: size) %>">
Expand Down Expand Up @@ -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")
Expand All @@ -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
<div class="<%= style(:sub, mode: :white, size: :md) %>">Hello</div>
Expand All @@ -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 {
Expand All @@ -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

0 comments on commit 2cab138

Please sign in to comment.