Skip to content

Commit

Permalink
Merge pull request #15 from javierav/feature/block-slots
Browse files Browse the repository at this point in the history
Add support for slots and advanced configuration via code block
  • Loading branch information
javierav authored Oct 24, 2024
2 parents 75a5d70 + 5750d63 commit f3ef3d2
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 94 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Unreleased
- Add support for slots ([#15](https://github.com/avo-hq/class_variants/pull/15))

## 0.0.8 (2024-10-24)
- Deprecate usage of positional arguments ([#12](https://github.com/avo-hq/class_variants/pull/12))
Expand Down
103 changes: 66 additions & 37 deletions lib/class_variants/instance.rb
Original file line number Diff line number Diff line change
@@ -1,41 +1,27 @@
module ClassVariants
class Instance
attr_reader :base, :variants, :compound_variants, :defaults

# rubocop:disable Naming/VariableName
def initialize(classes = nil, base: nil, variants: {}, compoundVariants: [], compound_variants: [], defaults: {})
warn <<~MSG if classes
(ClassVariants) DEPRECATION WARNING: Use of positional argument for default classes is deprecated
and will be removed in the next version. Use the `base` keyword argument instead.
MSG

warn <<~MSG unless compoundVariants.empty?
(ClassVariants) DEPRECATION WARNING: Use of `compoundVariants` keyword argument is deprecated
and will be removed in the next version. Use the `compound_variant` instead.
MSG

@base = base || classes
@variants = expand_boolean_variants(variants)
@compound_variants = compound_variants.empty? ? compoundVariants : compound_variants
@defaults = defaults
def initialize(**options, &block)
raise ArgumentError, "Use of hash config and code block is not supported" if !options.empty? && block_given?

@base = options.empty? ? {} : {default: options.fetch(:base, nil)}
@variants = expand_variants(options.fetch(:variants, {})) + expand_compound_variants(options.fetch(:compound_variants, []))
@defaults = options.fetch(:defaults, {})

instance_eval(&block) if block_given?
end
# rubocop:enable Naming/VariableName

def render(**overrides)
def render(slot = :default, **overrides)
# Start with our default classes
result = [@base]
result = [@base[slot]]

# Then merge the passed in overrides on top of the defaults
selected = @defaults.merge(overrides)
criteria = @defaults.merge(overrides)

selected.each do |variant_type, variant|
# dig the classes out and add them to the result
result << @variants.dig(variant_type, variant)
end
@variants.each do |candidate|
next unless candidate[:slot] == slot

@compound_variants.each do |compound_variant|
if (compound_variant.keys - [:class]).all? { |key| selected[key] == compound_variant[key] }
result << compound_variant[:class]
if (candidate.keys - [:class, :slot]).all? { |key| criteria[key] == candidate[key] }
result << candidate[:class]
end
end

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

private

def expand_boolean_variants(variants)
expanded = variants.map do |key, value|
case value
def base(klass = nil, &block)
raise ArgumentError, "Use of positional argument and code block is not supported" if klass && block_given?

if block_given?
with_slots(&block).each do |slot|
@base[slot[:slot]] = slot[:class]
end
else
@base[:default] = klass
end
end

def variant(**options, &block)
raise ArgumentError, "Use of class option and code block is not supported" if options.key?(:class) && block_given?

if block_given?
with_slots(&block).each do |slot|
@variants << options.merge(slot)
end
else
@variants << options.merge(slot: :default)
end
end

def defaults(**options)
@defaults = options
end

def slot(name = :default, **options)
raise ArgumentError, "class option is required" unless options.key?(:class)

@slots << options.merge(slot: name)
end

def with_slots
@slots = []
yield
@slots
end

def expand_variants(variants)
variants.flat_map do |property, values|
case values
when String
s_key = key.to_s
{s_key.delete_prefix("!").to_sym => {!s_key.start_with?("!") => value}}
{property.to_s.delete_prefix("!").to_sym => !property.to_s.start_with?("!"), :class => values, :slot => :default}
else
{key => value}
values.map do |key, value|
{property => key, :class => value, :slot => :default}
end
end
end
end

expanded.reduce do |output, next_variant|
output.merge!(next_variant) { |_key, v1, v2| v1.merge!(v2) }
def expand_compound_variants(compound_variants)
compound_variants.map do |compound_variant|
compound_variant.merge(slot: :default)
end
end
end
Expand Down
51 changes: 51 additions & 0 deletions test/block_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require "test_helper"

class BlockTest < Minitest::Test
def setup
@cv = ClassVariants.build do
base "text-white py-1 px-3 rounded-full"

variant color: :primary, class: "bg-blue-500"
variant color: :secondary, class: "bg-purple-500"
variant color: :success, class: "bg-green-500"

variant size: :sm, class: "py-1 px-3 text-xs"
variant size: :md, class: "py-1.5 px-4 text-sm"
variant size: :lg, class: "py-2 px-6 text-md"

variant disabled: true, class: "opacity-50 bg-gray-500"
variant visible: false, class: "hidden"

variant color: :success, disabled: true, class: "bg-green-100 text-green-700"

defaults size: :sm
end
end

def test_render_with_defaults
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render
end

def test_render_with_size
assert_equal "text-white py-1 px-3 rounded-full py-1.5 px-4 text-sm", @cv.render(size: :md)
end

def test_render_with_size_and_color
assert_equal(
"text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs",
@cv.render(size: :sm, color: :success)
)
end

def test_boolean_variants
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render(visible: true)
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs hidden", @cv.render(visible: false)
end

def test_compound_variants
assert_equal(
"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",
@cv.render(color: :success, disabled: true)
)
end
end
57 changes: 0 additions & 57 deletions test/class_variants_test.rb

This file was deleted.

56 changes: 56 additions & 0 deletions test/hash_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
require "test_helper"

class HashTest < Minitest::Test
def setup
@cv = ClassVariants.build(
base: "text-white py-1 px-3 rounded-full",
variants: {
color: {
primary: "bg-blue-500",
secondary: "bg-purple-500",
success: "bg-green-500"
},
size: {
sm: "py-1 px-3 text-xs",
md: "py-1.5 px-4 text-sm",
lg: "py-2 px-6 text-md"
},
disabled: "opacity-50 bg-gray-500",
"!visible": "hidden"
},
compound_variants: [
{color: :success, disabled: true, class: "bg-green-100 text-green-700"}
],
defaults: {
size: :sm
}
)
end

def test_render_with_defaults
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render
end

def test_render_with_size
assert_equal "text-white py-1 px-3 rounded-full py-1.5 px-4 text-sm", @cv.render(size: :md)
end

def test_render_with_size_and_color
assert_equal(
"text-white py-1 px-3 rounded-full bg-green-500 py-1 px-3 text-xs",
@cv.render(size: :sm, color: :success)
)
end

def test_boolean_variants
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs", @cv.render(visible: true)
assert_equal "text-white py-1 px-3 rounded-full py-1 px-3 text-xs hidden", @cv.render(visible: false)
end

def test_compound_variants
assert_equal(
"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",
@cv.render(color: :success, disabled: true)
)
end
end
69 changes: 69 additions & 0 deletions test/slot_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require "test_helper"

class SlotTest < Minitest::Test
def setup
@cv = ClassVariants.build do
base do
slot :root, class: "rounded py-3 px-5 mb-4"
slot :title, class: "font-bold mb-1"
end

variant variant: :outlined do
slot :root, class: "border"
end

variant variant: :outlined, severity: :error do
slot :root, class: "border-red-700 dark:border-red-500"
slot :title, class: "text-red-700 dark:text-red-500"
slot :message, class: "text-red-600 dark:text-red-500"
end

variant variant: :outlined, severity: :success do
slot :root, class: "border-green-700 dark:border-green-500"
slot :title, class: "text-green-700 dark:text-green-500"
slot :message, class: "text-green-600 dark:text-green-500"
end

variant variant: :filled, severity: :error do
slot :root, class: "bg-red-100 dark:bg-red-800"
slot :title, class: "text-red-900 dark:text-red-50"
slot :message, class: "text-red-700 dark:text-red-200"
end

variant variant: :filled, severity: :success do
slot :root, class: "bg-green-100 dark:bg-green-800"
slot :title, class: "text-green-900 dark:text-green-50"
slot :message, class: "text-green-700 dark:text-green-200"
end

defaults variant: :filled, severity: :success
end
end

def test_render_default_slot
assert_equal "", @cv.render
end

def test_render_nonexistent_slot
assert_equal "", @cv.render(:nonexistent)
end

def test_render_slot_with_defaults
assert_equal "rounded py-3 px-5 mb-4 bg-green-100 dark:bg-green-800", @cv.render(:root)
end

def test_render_slot_with_variant
assert_equal "rounded py-3 px-5 mb-4 border border-green-700 dark:border-green-500", @cv.render(:root, variant: :outlined)
end

def test_render_slot_without_base
assert_equal "text-green-700 dark:text-green-200", @cv.render(:message)
end

def test_render_slot_with_unused_variant
assert_equal(
"rounded py-3 px-5 mb-4 border border-green-700 dark:border-green-500",
@cv.render(:root, variant: :outlined, type: :button)
)
end
end

0 comments on commit f3ef3d2

Please sign in to comment.