Skip to content

Commit 5c5fe0b

Browse files
committed
Add support for slots
1 parent 75a5d70 commit 5c5fe0b

File tree

5 files changed

+242
-94
lines changed

5 files changed

+242
-94
lines changed

lib/class_variants/instance.rb

+66-37
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,27 @@
11
module ClassVariants
22
class Instance
3-
attr_reader :base, :variants, :compound_variants, :defaults
4-
5-
# rubocop:disable Naming/VariableName
6-
def initialize(classes = nil, base: nil, variants: {}, compoundVariants: [], compound_variants: [], defaults: {})
7-
warn <<~MSG if classes
8-
(ClassVariants) DEPRECATION WARNING: Use of positional argument for default classes is deprecated
9-
and will be removed in the next version. Use the `base` keyword argument instead.
10-
MSG
11-
12-
warn <<~MSG unless compoundVariants.empty?
13-
(ClassVariants) DEPRECATION WARNING: Use of `compoundVariants` keyword argument is deprecated
14-
and will be removed in the next version. Use the `compound_variant` instead.
15-
MSG
16-
17-
@base = base || classes
18-
@variants = expand_boolean_variants(variants)
19-
@compound_variants = compound_variants.empty? ? compoundVariants : compound_variants
20-
@defaults = defaults
3+
def initialize(**options, &block)
4+
raise ArgumentError, "Use of hash config and code block is not supported" if !options.empty? && block_given?
5+
6+
@base = options.empty? ? {} : {default: options.fetch(:base, nil)}
7+
@variants = expand_variants(options.fetch(:variants, {})) + expand_compound_variants(options.fetch(:compound_variants, []))
8+
@defaults = options.fetch(:defaults, {})
9+
10+
instance_eval(&block) if block_given?
2111
end
22-
# rubocop:enable Naming/VariableName
2312

24-
def render(**overrides)
13+
def render(slot = :default, **overrides)
2514
# Start with our default classes
26-
result = [@base]
15+
result = [@base[slot]]
2716

2817
# Then merge the passed in overrides on top of the defaults
29-
selected = @defaults.merge(overrides)
18+
criteria = @defaults.merge(overrides)
3019

31-
selected.each do |variant_type, variant|
32-
# dig the classes out and add them to the result
33-
result << @variants.dig(variant_type, variant)
34-
end
20+
@variants.each do |candidate|
21+
next unless candidate[:slot] == slot
3522

36-
@compound_variants.each do |compound_variant|
37-
if (compound_variant.keys - [:class]).all? { |key| selected[key] == compound_variant[key] }
38-
result << compound_variant[:class]
23+
if (candidate.keys - [:class, :slot]).all? { |key| criteria[key] == candidate[key] }
24+
result << candidate[:class]
3925
end
4026
end
4127

@@ -48,19 +34,62 @@ def render(**overrides)
4834

4935
private
5036

51-
def expand_boolean_variants(variants)
52-
expanded = variants.map do |key, value|
53-
case value
37+
def base(klass = nil, &block)
38+
raise ArgumentError, "Use of positional argument and code block is not supported" if klass && block_given?
39+
40+
if block_given?
41+
with_slots(&block).each do |slot|
42+
@base[slot[:slot]] = slot[:class]
43+
end
44+
else
45+
@base[:default] = klass
46+
end
47+
end
48+
49+
def variant(**options, &block)
50+
raise ArgumentError, "Use of class option and code block is not supported" if options.key?(:class) && block_given?
51+
52+
if block_given?
53+
with_slots(&block).each do |slot|
54+
@variants << options.merge(slot)
55+
end
56+
else
57+
@variants << options.merge(slot: :default)
58+
end
59+
end
60+
61+
def defaults(**options)
62+
@defaults = options
63+
end
64+
65+
def slot(name = :default, **options)
66+
raise ArgumentError, "class option is required" unless options.key?(:class)
67+
68+
@slots << options.merge(slot: name)
69+
end
70+
71+
def with_slots
72+
@slots = []
73+
yield
74+
@slots
75+
end
76+
77+
def expand_variants(variants)
78+
variants.flat_map do |property, values|
79+
case values
5480
when String
55-
s_key = key.to_s
56-
{s_key.delete_prefix("!").to_sym => {!s_key.start_with?("!") => value}}
81+
{property.to_s.delete_prefix("!").to_sym => !property.to_s.start_with?("!"), :class => values, :slot => :default}
5782
else
58-
{key => value}
83+
values.map do |key, value|
84+
{property => key, :class => value, :slot => :default}
85+
end
5986
end
6087
end
88+
end
6189

62-
expanded.reduce do |output, next_variant|
63-
output.merge!(next_variant) { |_key, v1, v2| v1.merge!(v2) }
90+
def expand_compound_variants(compound_variants)
91+
compound_variants.map do |compound_variant|
92+
compound_variant.merge(slot: :default)
6493
end
6594
end
6695
end

test/block_test.rb

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require "test_helper"
2+
3+
class BlockTest < Minitest::Test
4+
def setup
5+
@cv = ClassVariants.build do
6+
base "text-white py-1 px-3 rounded-full"
7+
8+
variant color: :primary, class: "bg-blue-500"
9+
variant color: :secondary, class: "bg-purple-500"
10+
variant color: :success, class: "bg-green-500"
11+
12+
variant size: :sm, class: "py-1 px-3 text-xs"
13+
variant size: :md, class: "py-1.5 px-4 text-sm"
14+
variant size: :lg, class: "py-2 px-6 text-md"
15+
16+
variant disabled: true, class: "opacity-50 bg-gray-500"
17+
variant visible: false, class: "hidden"
18+
19+
variant color: :success, disabled: true, class: "bg-green-100 text-green-700"
20+
21+
defaults size: :sm
22+
end
23+
end
24+
25+
def test_render_with_defaults
26+
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render
27+
end
28+
29+
def test_render_with_size
30+
assert_equal "text-white py-1 px-3 rounded-full py-1.5 px-4 text-sm", @cv.render(size: :md)
31+
end
32+
33+
def test_render_with_size_and_color
34+
assert_equal(
35+
"text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs",
36+
@cv.render(size: :sm, color: :success)
37+
)
38+
end
39+
40+
def test_boolean_variants
41+
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render(visible: true)
42+
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs hidden", @cv.render(visible: false)
43+
end
44+
45+
def test_compound_variants
46+
assert_equal(
47+
"text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs opacity-50 bg-gray-500 bg-green-100 text-green-700",
48+
@cv.render(color: :success, disabled: true)
49+
)
50+
end
51+
end

test/class_variants_test.rb

-57
This file was deleted.

test/hash_test.rb

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
require "test_helper"
2+
3+
class HashTest < Minitest::Test
4+
def setup
5+
@cv = ClassVariants.build(
6+
base: "text-white py-1 px-3 rounded-full",
7+
variants: {
8+
color: {
9+
primary: "bg-blue-500",
10+
secondary: "bg-purple-500",
11+
success: "bg-green-500"
12+
},
13+
size: {
14+
sm: "py-1 px-3 text-xs",
15+
md: "py-1.5 px-4 text-sm",
16+
lg: "py-2 px-6 text-md"
17+
},
18+
disabled: "opacity-50 bg-gray-500",
19+
"!visible": "hidden"
20+
},
21+
compound_variants: [
22+
{color: :success, disabled: true, class: "bg-green-100 text-green-700"}
23+
],
24+
defaults: {
25+
size: :sm
26+
}
27+
)
28+
end
29+
30+
def test_render_with_defaults
31+
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render
32+
end
33+
34+
def test_render_with_size
35+
assert_equal "text-white py-1 px-3 rounded-full py-1.5 px-4 text-sm", @cv.render(size: :md)
36+
end
37+
38+
def test_render_with_size_and_color
39+
assert_equal(
40+
"text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs",
41+
@cv.render(size: :sm, color: :success)
42+
)
43+
end
44+
45+
def test_boolean_variants
46+
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render(visible: true)
47+
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs hidden", @cv.render(visible: false)
48+
end
49+
50+
def test_compound_variants
51+
assert_equal(
52+
"text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs opacity-50 bg-gray-500 bg-green-100 text-green-700",
53+
@cv.render(color: :success, disabled: true)
54+
)
55+
end
56+
end

test/slot_test.rb

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
require "test_helper"
2+
3+
class SlotTest < Minitest::Test
4+
def setup
5+
@cv = ClassVariants.build do
6+
base do
7+
slot :root, class: "rounded py-3 px-5 mb-4"
8+
slot :title, class: "font-bold mb-1"
9+
end
10+
11+
variant variant: :outlined do
12+
slot :root, class: "border"
13+
end
14+
15+
variant variant: :outlined, severity: :error do
16+
slot :root, class: "border-red-700 dark:border-red-500"
17+
slot :title, class: "text-red-700 dark:text-red-500"
18+
slot :message, class: "text-red-600 dark:text-red-500"
19+
end
20+
21+
variant variant: :outlined, severity: :success do
22+
slot :root, class: "border-green-700 dark:border-green-500"
23+
slot :title, class: "text-green-700 dark:text-green-500"
24+
slot :message, class: "text-green-600 dark:text-green-500"
25+
end
26+
27+
variant variant: :filled, severity: :error do
28+
slot :root, class: "bg-red-100 dark:bg-red-800"
29+
slot :title, class: "text-red-900 dark:text-red-50"
30+
slot :message, class: "text-red-700 dark:text-red-200"
31+
end
32+
33+
variant variant: :filled, severity: :success do
34+
slot :root, class: "bg-green-100 dark:bg-green-800"
35+
slot :title, class: "text-green-900 dark:text-green-50"
36+
slot :message, class: "text-green-700 dark:text-green-200"
37+
end
38+
39+
defaults variant: :filled, severity: :success
40+
end
41+
end
42+
43+
def test_render_default_slot
44+
assert_equal "", @cv.render
45+
end
46+
47+
def test_render_nonexistent_slot
48+
assert_equal "", @cv.render(:nonexistent)
49+
end
50+
51+
def test_render_slot_with_defaults
52+
assert_equal "rounded py-3 px-5 mb-4 bg-green-100 dark:bg-green-800", @cv.render(:root)
53+
end
54+
55+
def test_render_slot_with_variant
56+
assert_equal "rounded py-3 px-5 mb-4 border border-green-700 dark:border-green-500", @cv.render(:root, variant: :outlined)
57+
end
58+
59+
def test_render_slot_without_base
60+
assert_equal "text-green-700 dark:text-green-200", @cv.render(:message)
61+
end
62+
63+
def test_render_slot_with_unused_variant
64+
assert_equal(
65+
"rounded py-3 px-5 mb-4 border border-green-700 dark:border-green-500",
66+
@cv.render(:root, variant: :outlined, type: :button)
67+
)
68+
end
69+
end

0 commit comments

Comments
 (0)