Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add inheritance strategies to style variants #50

Merged
merged 2 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## master


- Add inheritance strategies to style variants ([@omarluq][])

- Add special `class:` variant to `style` helper. For appending classes.
Inspired by https://cva.style/docs/getting-started/extending-components

Expand Down
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,69 @@ class ButtonComponent < ViewComponent::Base
end
```

### Style variants inheritance

Style variants support three inheritance strategies when extending components:

1. `override` (default behavior): Completely replaces parent variants.
2. `merge` (deep merge): Preserves all variant keys unless explicitly overwritten.
3. `extend` (shallow merge): Preserves variants unless explicitly overwritten.

Consider an example:

```ruby
class Parent::Component < ViewComponent::Base
include ViewComponentContrib::StyleVariants

style do
variants do
size {
md { 'text-md' }
lg { 'text-lg' }
}
disabled {
yes { "opacity-50" }
}
end
end
end

# Using override strategy (default)
class Child::Component < Parent::Component
style do
variants do
size {
lg { 'text-larger' }
}
end
end
end

# Using merge strategy
class Child::Component < Parent::Component
style do
variants(strategy: :merge) do
size {
lg { 'text-larger' }
}
end
end
end

# Using extend strategy
class Child::Component < Parent::Component
style do
variants(strategy: :extend) do
size {
lg { 'text-larger' }
}
end
end
end
```

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.

### Dependent (or compound) styles

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:
Expand Down
35 changes: 32 additions & 3 deletions lib/view_component_contrib/style_variants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ def initialize(&init_block)
@variants = {}
@compounds = {}

instance_eval(&init_block) if init_block
return unless init_block

@init_block = init_block
instance_eval(&init_block)
end

def base(&block)
Expand All @@ -88,8 +91,34 @@ def defaults(&block)
@defaults = block.call.freeze
end

def variants(&block)
@variants = VariantBuilder.new(true).build(&block)
def variants(strategy: :override, &block)
variants = build_variants(&block)
@variants = handle_variants(variants, strategy)
end

def build_variants(&block)
VariantBuilder.new(true).build(&block)
end

def handle_variants(variants, strategy)
return variants if strategy == :override

parent_variants = find_parent_variants
return variants unless parent_variants

return parent_variants.deep_merge(variants) if strategy == :merge

parent_variants.merge(variants) if strategy == :extend
end

def find_parent_variants
parent_component = @init_block.binding.receiver.superclass
return unless parent_component.respond_to?(:style_config)

parent_config = parent_component.style_config
default_parent_style = parent_component.default_style_name
parent_style_set = parent_config.instance_variable_get(:@styles)[default_parent_style.to_sym]
parent_style_set.instance_variable_get(:@variants).deep_dup
end

def compound(**variants, &block)
Expand Down
58 changes: 56 additions & 2 deletions test/cases/style_variants_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,61 @@ def test_inheritance
assert_css "a.text-black"
end

class PostProccesedComponent < Component
class SubComponentMerge < Component
erb_template <<~ERB
<div class="<%= style(:component, theme: theme, size: size, disabled: disabled) %>">Hello</div>
ERB
style :component do
base { "cursor-pointer" }

variants(strategy: :merge) {
size {
lg { %w[text-larger] }
}
}
end
end

def test_inheritance_with_merge_strategy
# test new lg style
component = SubComponentMerge.new(theme: :secondary, size: :lg)
render_inline(component)
assert_css "div.secondary-color.secondary-bg.text-larger"

# test inherited md styule
component = SubComponentMerge.new(theme: :secondary, size: :md)
render_inline(component)
assert_css "div.secondary-color.secondary-bg.text-md"
end

class SubComponentExtend < Component
erb_template <<~ERB
<div class="<%= style(:component, theme: theme, size: size, disabled: disabled) %>">Hello</div>
ERB
style :component do
base { "cursor-pointer" }

variants(strategy: :extend) {
size {
lg { %w[text-larger] }
}
}
end
end

def test_inheritance_with_extend_strategy
# test new lg style
component = SubComponentExtend.new(theme: :secondary, size: :lg)
render_inline(component)
assert_css "div.secondary-color.secondary-bg.text-larger"

# test does not inherit md styule
component = SubComponentExtend.new(theme: :secondary, size: :md)
render_inline(component)
assert_no_css "div.secondary-color.secondary-bg.text-md"
end

class PostProcessedComponent < Component
style_config.postprocess_with do |compiled|
compiled.join(" ").gsub("primary", "karamba")
end
Expand All @@ -116,7 +170,7 @@ class PostProccesedComponent < Component
end

def test_postprocessor
component = PostProccesedComponent.new
component = PostProcessedComponent.new

render_inline(component)

Expand Down
Loading