Skip to content

Commit ca472b6

Browse files
committed
Add support for slots
1 parent e5e3535 commit ca472b6

File tree

6 files changed

+243
-88
lines changed

6 files changed

+243
-88
lines changed

lib/class_variants.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
module ClassVariants
77
class << self
8-
def build(*args, **kwargs)
9-
Instance.new(*args, **kwargs)
8+
def build(...)
9+
Instance.new(...)
1010
end
1111
end
1212
end

lib/class_variants/instance.rb

+65-29
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,29 @@
11
module ClassVariants
22
class Instance
3-
attr_reader :base, :variants, :compoundVariants, :defaults
4-
53
# rubocop:disable Naming/VariableName
6-
def initialize(classes = nil, base: nil, variants: {}, compoundVariants: [], 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 major version. Use the `base` keyword argument instead.
10-
MSG
4+
def initialize(**options, &block)
5+
raise ArgumentError, "Use of hash config and code block is not supported" if !options.empty? && block_given?
6+
7+
@base = options.empty? ? {} : {default: options.fetch(:base, nil)}
8+
@variants = expand_variants(options.fetch(:variants, {})) + expand_compound_variants(options.fetch(:compoundVariants, []))
9+
@defaults = options.fetch(:defaults, {})
1110

12-
@base = base || classes
13-
@variants = expand_boolean_variants(variants)
14-
@compoundVariants = compoundVariants
15-
@defaults = defaults
11+
instance_eval(&block) if block_given?
1612
end
1713
# rubocop:enable Naming/VariableName
1814

19-
def render(**overrides)
15+
def render(slot = :default, **overrides)
2016
# Start with our default classes
21-
result = [@base]
17+
result = [@base[slot]]
2218

2319
# Then merge the passed in overrides on top of the defaults
24-
selected = @defaults.merge(overrides)
20+
criteria = @defaults.merge(overrides)
2521

26-
selected.each do |variant_type, variant|
27-
# dig the classes out and add them to the result
28-
result << @variants.dig(variant_type, variant)
29-
end
22+
@variants.each do |candidate|
23+
next unless candidate[:slot] == slot
3024

31-
@compoundVariants.each do |compound_variant|
32-
if (compound_variant.keys - [:class]).all? { |key| selected[key] == compound_variant[key] }
33-
result << compound_variant[:class]
25+
if (candidate.keys - [:class, :slot]).all? { |key| criteria[key] == candidate[key] }
26+
result << candidate[:class]
3427
end
3528
end
3629

@@ -43,19 +36,62 @@ def render(**overrides)
4336

4437
private
4538

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

57-
expanded.reduce do |output, next_variant|
58-
output.merge!(next_variant) { |_key, v1, v2| v1.merge!(v2) }
92+
def expand_compound_variants(compound_variants)
93+
compound_variants.map do |compound_variant|
94+
compound_variant.merge(slot: :default)
5995
end
6096
end
6197
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+
compoundVariants: [
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)