Skip to content

meldui/twm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TWM (Tailwind Merge)

TWM is an Elixir port of the popular JavaScript/TypeScript tailwind-merge library. It efficiently merges Tailwind CSS classes without style conflicts by intelligently handling conflicting utilities.

Attribution

This library is a port of the excellent tailwind-merge library (v3.3.0) by Dany Castillo. The original JavaScript/TypeScript implementation provides the foundation and algorithms that make this Elixir version possible.

Original Library:

Purpose

TWM solves the problem of conflicting Tailwind CSS classes when dynamically combining class strings. When you have multiple sources of Tailwind classes (props, conditional logic, component composition), you often end up with conflicts like:

# Without TWM - both padding classes are applied, causing unexpected results
"px-2 px-4"  # Both px-2 and px-4 are in the final output

# With TWM - conflicts are resolved intelligently
Twm.merge("px-2 px-4")
# => "px-4"  # Only the last conflicting class is kept

Features

  • Conflict Resolution: Automatically removes conflicting Tailwind classes
  • Performance Optimized: Built-in LRU cache for repeated class combinations
  • Extensible: Support for custom configurations and class groups
  • Multiple Input Types: Accepts strings, lists, and nested structures
  • Arbitrary Values: Full support for Tailwind's arbitrary value syntax
  • TypeScript Compatibility: Maintains API compatibility with the original library
  • Elixir Native: Leverages Elixir's strengths for better performance and reliability

Installation

Add twm to your list of dependencies in mix.exs:

def deps do
  [
    {:twm, "~> 0.1.0"}
  ]
end

Then run:

mix deps.get

Quick Start

# Basic usage - resolves padding conflicts
Twm.merge("px-2 py-1 px-3")
# => "py-1 px-3"

# Background color conflicts
Twm.merge("bg-red-500 bg-blue-500")
# => "bg-blue-500"

# Complex spacing conflicts
Twm.merge("pt-2 pt-4 pb-3")
# => "pt-4 pb-3"

# Works with lists too
Twm.merge(["flex", "items-center", "justify-center"])
# => "flex items-center justify-center"

# Handles arbitrary values
Twm.merge("p-[20px] p-[30px]")
# => "p-[30px]"

# Modifier conflicts
Twm.merge("hover:bg-red-500 hover:bg-blue-500")
# => "hover:bg-blue-500"

Usage Examples

With Cache (Default Behavior)

The library automatically uses an LRU cache to optimize performance for repeated class combinations:

# First call - computes and caches result
result1 = Twm.merge("px-2 py-1 px-3")
# => "py-1 px-3"

# Second call with same input - returns cached result (faster)
result2 = Twm.merge("px-2 py-1 px-3")
# => "py-1 px-3" (from cache)

# Check cache usage
Twm.Cache.size()  # Returns number of cached entries

Without Cache (Custom Configuration)

For scenarios where you don't want caching, you can create a custom configuration:

# Create a configuration without cache
no_cache_config = Twm.Config.extend(cache_size: 0)

# Use the configuration directly
Twm.merge("px-2 px-3", no_cache_config)
# => "px-3" (no caching)

Different Input Types

# String input
Twm.merge("flex items-center px-4 px-2")
# => "flex items-center px-2"

# List input
Twm.merge(["flex", "items-center", "px-4", "px-2"])
# => "flex items-center px-2"

# Mixed with nils and false values (filtered out)
Twm.merge(["flex", nil, "items-center", false, "px-4"])
# => "flex items-center px-4"

# Empty inputs
Twm.merge("")        # => ""
Twm.merge([])        # => ""
Twm.merge([nil])     # => ""

Configuration

Ways to Create Config Structures

TWM provides several ways to create and customize configurations:

1. Using the Default Configuration

# Get the default configuration
default_config = Twm.Config.get_default()

# Use with merge
Twm.merge("px-2 px-4", default_config)
# => "px-4"

2. Creating a New Configuration from Scratch

# Create a completely custom configuration
custom_config = Twm.Config.new(
  cache_size: 100,
  theme: [],
  class_groups: [
    # Define custom class groups
    spacing: ["p-1", "p-2", "p-4", "p-8"],
    colors: ["text-red", "text-blue", "text-green"]
  ],
  conflicting_class_groups: [
    # Define which groups conflict with each other
    spacing: ["margins"],
    colors: ["backgrounds"]
  ],
  conflicting_class_group_modifiers: [],
  order_sensitive_modifiers: []
)

Twm.merge("p-1 p-4", custom_config)
# => "p-4"

3. Extending the Default Configuration

# Extend with simple options
extended_config = Twm.Config.extend(
  cache_size: 1000,
  prefix: "tw-"
)

# Extend with override (replaces default values)
override_config = Twm.Config.extend(
  override: [
    class_groups: [
      display: ["custom-block", "custom-flex"]
    ]
  ]
)

# Extend with additional values (merges with defaults)
extended_config = Twm.Config.extend(
  extend: [
    class_groups: [
      custom_group: ["custom-class-1", "custom-class-2"]
    ],
    conflicting_class_groups: [
      custom_group: ["display"]
    ]
  ]
)

4. Using Configuration Functions

# Create a configuration with a function
config_with_fn = Twm.Config.extend(
  default_config,
  fn config ->
    # Modify the configuration
    config
    |> Keyword.update!(:class_groups, fn groups ->
      Keyword.put(groups, :custom_utilities, ["util-1", "util-2"])
    end)
    |> Keyword.update!(:conflicting_class_groups, fn conflicts ->
      Keyword.put(conflicts, :custom_utilities, ["display"])
    end)
  end
)

Performance and Caching

Cache Configuration

  1. As a global cache for the main Twm functionality:
defmodule Twm.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Start the Twm.Cache with default configuration
      {Twm.Cache, []}
    ]

    opts = [strategy: :one_for_one, name: Twm.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

You can also pass a custom cache name and configuration:

defmodule Twm.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    # Custom configuration
    custom_config = Twm.Config.extend(cache_size: 1000)

    children = [
      # Start the Twm.Cache with custom name and configuration
      {Twm.Cache, [name: :my_twm_cache, config: custom_config]}
    ]

    opts = [strategy: :one_for_one, name: Twm.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
# Configure cache size
large_cache_config = Twm.Config.extend(cache_size: 2000)

# Use a custom cache name
custom_cache_config = Twm.Config.extend(cache_name: MyApp.TwmCache)

# Disable caching entirely
no_cache_config = Twm.Config.extend(cache_size: 0)

Cache Management

# Check cache size
Twm.Cache.size()

# Clear cache
Twm.Cache.clear()

# Resize cache
Twm.Cache.resize(1000)

# Get cache statistics (for monitoring)
{:ok, state} = Twm.Cache.get_state()

Benchmarking

Run benchmarks to test performance in your environment:

# Quick development benchmarks
mix run test/benchmarks/quick_benchmark.exs

# Comprehensive benchmarks
mix run test/benchmarks/benchmark.exs

Advanced Usage

Working with Arbitrary Values

# Arbitrary padding values
Twm.merge("p-[20px] p-[25px]")
# => "p-[25px]"

# Arbitrary colors
Twm.merge("bg-[#ff0000] bg-[#00ff00]")
# => "bg-[#00ff00]"

# Complex arbitrary values
Twm.merge("grid-cols-[200px_1fr_100px] grid-cols-[300px_1fr]")
# => "grid-cols-[300px_1fr]"

Modifier Handling

# Pseudo-class modifiers
Twm.merge("hover:bg-red-500 hover:bg-blue-500 focus:bg-green-500")
# => "hover:bg-blue-500 focus:bg-green-500"

# Responsive modifiers
Twm.merge("sm:p-2 md:p-4 sm:p-3")
# => "md:p-4 sm:p-3"

# Dark mode modifiers
Twm.merge("dark:text-white dark:text-gray-100")
# => "dark:text-gray-100"

# Stacked modifiers
Twm.merge("sm:hover:bg-red-500 sm:hover:bg-blue-500")
# => "sm:hover:bg-blue-500"

Complex Conflict Resolution

# Multiple property conflicts
Twm.merge("px-2 py-4 p-3 pt-6")
# => "px-2 py-4 p-3 pt-6" → "p-3 pt-6" (p-3 overrides px-2 py-4, pt-6 overrides p-3's top padding)

# Border conflicts
Twm.merge("border-2 border-4 border-t-8")
# => "border-4 border-t-8"

# Flexbox conflicts
Twm.merge("flex-row flex-col items-start items-center")
# => "flex-col items-center"

API Reference

Core Functions

  • Twm.merge/1 - Main merge function with default configuration
  • Twm.merge/3 - Merge with custom configuration and options
  • Twm.Config.get_default/0 - Get the default configuration
  • Twm.Config.new/1 - Create a new configuration from scratch
  • Twm.Config.extend/1 - Extend the default configuration
  • Twm.Config.extend/2 - Extend a configuration with a function

Cache Functions

  • Twm.Cache.start_link/1 - Start the cache GenServer
  • Twm.Cache.get/2 - Get a value from cache
  • Twm.Cache.put/3 - Put a value in cache
  • Twm.Cache.clear/1 - Clear all cache entries
  • Twm.Cache.size/1 - Get current cache size
  • Twm.Cache.resize/2 - Resize the cache

Validator Functions

The library includes comprehensive validators for different Tailwind value types:

  • Twm.is_arbitrary_value/1 - Check if value is arbitrary (e.g., [20px])
  • Twm.is_arbitrary_length/1 - Check if value is arbitrary length
  • Twm.is_arbitrary_number/1 - Check if value is arbitrary number
  • Twm.is_tshirt_size/1 - Check if value is t-shirt size (xs, sm, md, lg, xl, etc.)
  • Twm.is_integer/1 - Check if value is integer
  • Twm.is_fraction/1 - Check if value is fraction (1/2, 1/3, etc.)

Development

Running Tests

# Run all tests
mix test

# Run specific test file
mix test test/twm_test.exs

Code Quality

# Format code
mix format

# Run linter
mix credo

# Type checking (if dialyzer is configured)
mix dialyzer

Benchmarking

# Quick benchmarks for development
mix run scripts/quick_benchmark.exs

# Full benchmark suite
mix run test/twm_benchmark.exs

API Compatibility

The Elixir version maintains API compatibility where possible:

// TypeScript/JavaScript
import { twMerge } from 'tailwind-merge'
twMerge('px-2 py-1 px-3') // => 'py-1 px-3'
# Elixir
Twm.merge("px-2 py-1 px-3") # => "py-1 px-3"

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Add tests for your changes
  5. Ensure all tests pass (mix test)
  6. Ensure code formatting (mix format)
  7. Ensure code quality (mix credo)
  8. Commit your changes (git commit -am 'Add amazing feature')
  9. Push to the branch (git push origin feature/amazing-feature)
  10. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Dany Castillo and contributors to the original tailwind-merge library
  • The Tailwind CSS team for creating the utility-first CSS framework
  • The Elixir community for providing an excellent platform for this port

Note: This is a community-maintained port and is not officially affiliated with the original tailwind-merge project or Tailwind CSS.

About

Elixir port of Tailwind Merge

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages