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

Add support for slots and advanced configuration via code block #15

Merged
merged 1 commit into from
Oct 24, 2024
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
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