mirror of
https://github.com/twigphp/Twig.git
synced 2026-10-23 04:05:37 +00:00
Introduce CVA to html-extra
This commit is contained in:
committed by
Fabien Potencier
parent
21df1ad782
commit
e3ac14e1c8
@@ -0,0 +1,185 @@
|
||||
``html_cva``
|
||||
============
|
||||
|
||||
`CVA (Class Variant Authority)`_ is a concept from the JavaScript world and used
|
||||
by the well-known `shadcn/ui`_ library.
|
||||
The CVA concept is used to render multiple variations of components, applying
|
||||
a set of conditions and recipes to dynamically compose CSS class strings (color, size, etc.),
|
||||
to create highly reusable and customizable templates.
|
||||
|
||||
The concept of CVA is powered by a ``html_cva()`` Twig
|
||||
function where you define ``base`` classes that should always be present and then different
|
||||
``variants`` and the corresponding classes:
|
||||
|
||||
.. code-block:: html+twig
|
||||
|
||||
{# templates/alert.html.twig #}
|
||||
{% set alert = html_cva(
|
||||
base='alert ',
|
||||
variants={
|
||||
color: {
|
||||
blue: 'bg-blue',
|
||||
red: 'bg-red',
|
||||
green: 'bg-green',
|
||||
},
|
||||
size: {
|
||||
sm: 'text-sm',
|
||||
md: 'text-md',
|
||||
lg: 'text-lg',
|
||||
}
|
||||
}
|
||||
) %}
|
||||
|
||||
<div class="{{ alert.apply({color, size}, class) }}">
|
||||
...
|
||||
</div>
|
||||
|
||||
Then use the ``color`` and ``size`` variants to select the needed classes:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
{# index.html.twig #}
|
||||
{{ include('alert.html.twig', {'color': 'blue', 'size': 'md'}) }}
|
||||
// class="alert bg-red text-lg"
|
||||
|
||||
{{ include('alert.html.twig', {'color': 'green', 'size': 'sm'}) }}
|
||||
// class="alert bg-green text-sm"
|
||||
|
||||
{{ include('alert.html.twig', {'color': 'red', 'class': 'flex items-center justify-center'}) }}
|
||||
// class="alert bg-red text-md flex items-center justify-center"
|
||||
|
||||
CVA and Tailwind CSS
|
||||
--------------------
|
||||
|
||||
CVA work perfectly with Tailwind CSS. The only drawback is that you can have class conflicts.
|
||||
To "merge" conflicting classes together and keep only the ones you need, use the
|
||||
``tailwind_merge()`` filter from `tales-from-a-dev/twig-tailwind-extra`_
|
||||
with the ``html_cva()`` function:
|
||||
|
||||
.. code-block:: terminal
|
||||
|
||||
$ composer require tales-from-a-dev/twig-tailwind-extra
|
||||
|
||||
.. code-block:: html+twig
|
||||
|
||||
{% set alert = html_cva(
|
||||
// ...
|
||||
) %}
|
||||
|
||||
<div class="{{ alert.apply({color, size}, class)|tailwind_merge }}">
|
||||
...
|
||||
</div>
|
||||
|
||||
Compound Variants
|
||||
-----------------
|
||||
|
||||
You can define compound variants. A compound variant is a variant that applies
|
||||
when multiple other variant conditions are met:
|
||||
|
||||
.. code-block:: html+twig
|
||||
|
||||
{% set alert = html_cva(
|
||||
base='alert',
|
||||
variants={
|
||||
color: {
|
||||
blue: 'bg-blue',
|
||||
red: 'bg-red',
|
||||
green: 'bg-green',
|
||||
},
|
||||
size: {
|
||||
sm: 'text-sm',
|
||||
md: 'text-md',
|
||||
lg: 'text-lg',
|
||||
}
|
||||
},
|
||||
compoundVariants=[{
|
||||
// if color = red AND size = (md or lg), add the `font-bold` class
|
||||
color: ['red'],
|
||||
size: ['md', 'lg'],
|
||||
class: 'font-bold'
|
||||
}]
|
||||
) %}
|
||||
|
||||
<div class="{{ alert.apply({color, size}) }}">
|
||||
...
|
||||
</div>
|
||||
|
||||
{# index.html.twig #}
|
||||
|
||||
{{ include('alert.html.twig', {color: 'red', size: 'lg'}) }}
|
||||
// class="alert bg-red text-lg font-bold"
|
||||
|
||||
{{ include('alert.html.twig', {color: 'green', size: 'sm'}) }}
|
||||
// class="alert bg-green text-sm"
|
||||
|
||||
{{ include('alert.html.twig', {color: 'red', size: 'md'}) }}
|
||||
// class="alert bg-green text-lg font-bold"
|
||||
|
||||
Default Variants
|
||||
----------------
|
||||
|
||||
If no variants match, you can define a default set of classes to apply:
|
||||
|
||||
.. code-block:: html+twig
|
||||
|
||||
{% set alert = html_cva(
|
||||
base='alert ',
|
||||
variants={
|
||||
color: {
|
||||
blue: 'bg-blue',
|
||||
red: 'bg-red',
|
||||
green: 'bg-green',
|
||||
},
|
||||
size: {
|
||||
sm: 'text-sm',
|
||||
md: 'text-md',
|
||||
lg: 'text-lg',
|
||||
},
|
||||
rounded: {
|
||||
sm: 'rounded-sm',
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
}
|
||||
},
|
||||
defaultVariants={
|
||||
rounded: 'md',
|
||||
}
|
||||
) %}
|
||||
|
||||
<div class="{{ alert.apply({color, size}) }}">
|
||||
...
|
||||
</div>
|
||||
|
||||
{# index.html.twig #}
|
||||
|
||||
{{ include('alert.html.twig', {color: 'red', size: 'lg'}) }}
|
||||
// class="alert bg-red text-lg font-bold rounded-md"
|
||||
|
||||
.. note::
|
||||
|
||||
The ``html_cva`` function is part of the ``HtmlExtension`` which is not
|
||||
installed by default. Install it first:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ composer require twig/html-extra
|
||||
|
||||
Then, on Symfony projects, install the ``twig/extra-bundle``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ composer require twig/extra-bundle
|
||||
|
||||
Otherwise, add the extension explicitly on the Twig environment::
|
||||
|
||||
use Twig\Extra\Html\HtmlExtension;
|
||||
|
||||
$twig = new \Twig\Environment(...);
|
||||
$twig->addExtension(new HtmlExtension());
|
||||
|
||||
This function works best when used with `TwigComponent`_.
|
||||
|
||||
.. _`CVA (Class Variant Authority)`: https://cva.style/docs/getting-started/variants
|
||||
.. _`shadcn/ui`: https://ui.shadcn.com
|
||||
.. _`tales-from-a-dev/twig-tailwind-extra`: https://github.com/tales-from-a-dev/twig-tailwind-extra
|
||||
.. _`TwigComponent`: https://symfony.com/bundles/ux-twig-component/current/index.html
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Extra\Html;
|
||||
|
||||
/**
|
||||
* Class Variant Authority (CVA) resolver.
|
||||
*
|
||||
* @author Mathéo Daninos <matheo.daninos@gmail.com>
|
||||
*/
|
||||
final class Cva
|
||||
{
|
||||
/**
|
||||
* @var list<string|null>
|
||||
*/
|
||||
private array $base;
|
||||
|
||||
/**
|
||||
* @param string|list<string|null> $base The base classes to apply to the component
|
||||
*/
|
||||
public function __construct(
|
||||
string|array $base = [],
|
||||
/**
|
||||
* The variants to apply based on recipes.
|
||||
*
|
||||
* Format: [variantCategory => [variantName => classes]]
|
||||
*
|
||||
* Example:
|
||||
* 'colors' => [
|
||||
* 'primary' => 'bleu-8000',
|
||||
* 'danger' => 'red-800 text-bold',
|
||||
* ],
|
||||
* 'size' => [...],
|
||||
*
|
||||
* @var array<string, array<string, string|list<string>>>
|
||||
*/
|
||||
private array $variants = [],
|
||||
|
||||
/**
|
||||
* The compound variants to apply based on recipes.
|
||||
*
|
||||
* Format: [variantsCategory => ['variantName', 'variantName'], class: classes]
|
||||
*
|
||||
* Example:
|
||||
* [
|
||||
* 'colors' => ['primary'],
|
||||
* 'size' => ['small'],
|
||||
* 'class' => 'text-red-500',
|
||||
* ],
|
||||
* [
|
||||
* 'size' => ['large'],
|
||||
* 'class' => 'font-weight-500',
|
||||
* ]
|
||||
*
|
||||
* @var array<array<string, string|array<string>>>
|
||||
*/
|
||||
private array $compoundVariants = [],
|
||||
|
||||
/**
|
||||
* The default variants to apply if specific recipes aren't provided.
|
||||
*
|
||||
* Format: [variantCategory => variantName]
|
||||
*
|
||||
* Example:
|
||||
* 'colors' => 'primary',
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $defaultVariants = [],
|
||||
) {
|
||||
$this->base = (array) $base;
|
||||
}
|
||||
|
||||
public function apply(array $recipes, ?string ...$additionalClasses): string
|
||||
{
|
||||
$classes = $this->base;
|
||||
|
||||
// Resolve recipes against variants
|
||||
foreach ($recipes as $recipeName => $recipeValue) {
|
||||
if (\is_bool($recipeValue)) {
|
||||
$recipeValue = $recipeValue ? 'true' : 'false';
|
||||
}
|
||||
$recipeClasses = $this->variants[$recipeName][$recipeValue] ?? [];
|
||||
$classes = [...$classes, ...(array) $recipeClasses];
|
||||
}
|
||||
|
||||
// Resolve compound variants
|
||||
foreach ($this->compoundVariants as $compound) {
|
||||
$compoundClasses = $this->resolveCompoundVariant($compound, $recipes) ?? [];
|
||||
$classes = [...$classes, ...$compoundClasses];
|
||||
}
|
||||
|
||||
// Apply default variants if specific recipes aren't provided
|
||||
foreach ($this->defaultVariants as $defaultVariantName => $defaultVariantValue) {
|
||||
if (!isset($recipes[$defaultVariantName])) {
|
||||
$variantClasses = $this->variants[$defaultVariantName][$defaultVariantValue] ?? [];
|
||||
$classes = [...$classes, ...(array) $variantClasses];
|
||||
}
|
||||
}
|
||||
$classes = [...$classes, ...array_values($additionalClasses)];
|
||||
|
||||
$classes = implode(' ', array_filter($classes, 'is_string'));
|
||||
$classes = preg_split('#\s+#', $classes, -1, \PREG_SPLIT_NO_EMPTY) ?: [];
|
||||
|
||||
return implode(' ', array_unique($classes));
|
||||
}
|
||||
|
||||
private function resolveCompoundVariant(array $compound, array $recipes): array
|
||||
{
|
||||
foreach ($compound as $compoundName => $compoundValues) {
|
||||
if ('class' === $compoundName) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($recipes[$compoundName]) || !\in_array($recipes[$compoundName], (array) $compoundValues)) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return (array) ($compound['class'] ?? []);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ final class HtmlExtension extends AbstractExtension
|
||||
{
|
||||
return [
|
||||
new TwigFunction('html_classes', [self::class, 'htmlClasses']),
|
||||
new TwigFunction('html_cva', [self::class, 'htmlCva']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -110,4 +111,17 @@ final class HtmlExtension extends AbstractExtension
|
||||
|
||||
return implode(' ', array_unique(array_filter($classes, static function ($v) { return '' !== $v; })));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|list<string|null> $base
|
||||
* @param array<string, array<string, string|array<string>> $variants
|
||||
* @param array<array<string, string|array<string>>> $compoundVariants
|
||||
* @param array<string, string> $defaultVariant
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function htmlCva(array|string $base = [], array $variants = [], array $compoundVariants = [], array $defaultVariant = []): Cva
|
||||
{
|
||||
return new Cva($base, $variants, $compoundVariants, $defaultVariant);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,8 @@ This package is a Twig extension that provides the following:
|
||||
* [`html_classes`][2] function: returns a string by conditionally joining class
|
||||
names together.
|
||||
|
||||
* [`html_cva`][3] function: returns a `Cva` object to handle class variants.
|
||||
|
||||
[1]: https://twig.symfony.com/data_uri
|
||||
[2]: https://twig.symfony.com/html_classes
|
||||
[3]: https://twig.symfony.com/html_cva
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Twig.
|
||||
*
|
||||
* (c) Fabien Potencier
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Twig\Extra\Html\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Extra\Html\Cva;
|
||||
|
||||
class CvaTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider recipeProvider
|
||||
*/
|
||||
public function testRecipes(array $recipe, array $recipes, string $expected)
|
||||
{
|
||||
$recipeClass = new Cva($recipe['base'] ?? '', $recipe['variants'] ?? [], $recipe['compounds'] ?? [], $recipe['defaultVariants'] ?? []);
|
||||
|
||||
$this->assertEquals($expected, $recipeClass->apply($recipes));
|
||||
}
|
||||
|
||||
public function testApply()
|
||||
{
|
||||
$recipe = new Cva('font-semibold border rounded', [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
], [
|
||||
[
|
||||
'colors' => ['primary'],
|
||||
'sizes' => ['sm'],
|
||||
'class' => 'text-red-500',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals('font-semibold border rounded text-primary text-sm text-red-500', $recipe->apply(['colors' => 'primary', 'sizes' => 'sm']));
|
||||
}
|
||||
|
||||
public function testApplyWithNullString()
|
||||
{
|
||||
$recipe = new Cva('font-semibold border rounded', [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
], [
|
||||
[
|
||||
'colors' => ['primary'],
|
||||
'sizes' => ['sm'],
|
||||
'class' => 'text-red-500',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals('font-semibold border rounded text-primary text-sm text-red-500 flex justify-center', $recipe->apply(['colors' => 'primary', 'sizes' => 'sm'], 'flex', null, 'justify-center'));
|
||||
}
|
||||
|
||||
public static function recipeProvider(): iterable
|
||||
{
|
||||
yield 'base null' => [
|
||||
['variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
]],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'text-primary text-sm',
|
||||
];
|
||||
|
||||
yield 'base empty' => [
|
||||
[
|
||||
'base' => '',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
]],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'text-primary text-sm',
|
||||
];
|
||||
|
||||
yield 'base array' => [
|
||||
[
|
||||
'base' => ['font-semibold', 'border', 'rounded'],
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
]],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'font-semibold border rounded text-primary text-sm',
|
||||
];
|
||||
|
||||
yield 'no recipes match' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'red', 'sizes' => 'test'],
|
||||
'font-semibold border rounded',
|
||||
];
|
||||
|
||||
yield 'simple variants' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'font-semibold border rounded text-primary text-sm',
|
||||
];
|
||||
|
||||
yield 'simple variants as array' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => ['text-primary', 'uppercase'],
|
||||
'secondary' => ['text-secondary', 'uppercase'],
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'font-semibold border rounded text-primary uppercase text-sm',
|
||||
];
|
||||
|
||||
yield 'simple variants with custom' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'secondary', 'sizes' => 'md'],
|
||||
'font-semibold border rounded text-secondary text-md',
|
||||
];
|
||||
|
||||
yield 'compound variants' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
],
|
||||
'compounds' => [
|
||||
[
|
||||
'colors' => 'primary',
|
||||
'sizes' => ['sm'],
|
||||
'class' => 'text-red-100',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'font-semibold border rounded text-primary text-sm text-red-100',
|
||||
];
|
||||
|
||||
yield 'compound variants with true' => [
|
||||
[
|
||||
'base' => 'button',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'blue' => 'btn-blue',
|
||||
'red' => 'btn-red',
|
||||
],
|
||||
'disabled' => [
|
||||
'true' => 'disabled',
|
||||
],
|
||||
],
|
||||
'compounds' => [
|
||||
[
|
||||
'colors' => 'blue',
|
||||
'disabled' => ['true'],
|
||||
'class' => 'font-bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'blue', 'disabled' => 'true'],
|
||||
'button btn-blue disabled font-bold',
|
||||
];
|
||||
|
||||
yield 'compound variants as array' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
],
|
||||
'compounds' => [
|
||||
[
|
||||
'colors' => ['primary'],
|
||||
'sizes' => ['sm'],
|
||||
'class' => ['text-red-900', 'bold'],
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'font-semibold border rounded text-primary text-sm text-red-900 bold',
|
||||
];
|
||||
|
||||
yield 'multiple compound variants' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
],
|
||||
'compounds' => [
|
||||
[
|
||||
'colors' => ['primary'],
|
||||
'sizes' => ['sm'],
|
||||
'class' => 'text-red-300',
|
||||
],
|
||||
[
|
||||
'colors' => ['primary'],
|
||||
'sizes' => ['md'],
|
||||
'class' => 'text-blue-300',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'font-semibold border rounded text-primary text-sm text-red-300',
|
||||
];
|
||||
|
||||
yield 'compound with multiple variants' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
],
|
||||
'compounds' => [
|
||||
[
|
||||
'colors' => ['primary', 'secondary'],
|
||||
'sizes' => ['sm'],
|
||||
'class' => 'text-red-800',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'font-semibold border rounded text-primary text-sm text-red-800',
|
||||
];
|
||||
|
||||
yield 'compound doesn\'t match' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
],
|
||||
'compounds' => [
|
||||
[
|
||||
'colors' => ['danger', 'secondary'],
|
||||
'sizes' => ['sm'],
|
||||
'class' => 'text-red-500',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'font-semibold border rounded text-primary text-sm',
|
||||
];
|
||||
|
||||
yield 'default variables' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
'rounded' => [
|
||||
'sm' => 'rounded-sm',
|
||||
'md' => 'rounded-md',
|
||||
'lg' => 'rounded-lg',
|
||||
],
|
||||
],
|
||||
'compounds' => [
|
||||
[
|
||||
'colors' => ['danger', 'secondary'],
|
||||
'sizes' => 'sm',
|
||||
'class' => 'text-red-500',
|
||||
],
|
||||
],
|
||||
'defaultVariants' => [
|
||||
'colors' => 'primary',
|
||||
'sizes' => 'sm',
|
||||
'rounded' => 'md',
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'sizes' => 'sm'],
|
||||
'font-semibold border rounded text-primary text-sm rounded-md',
|
||||
];
|
||||
|
||||
yield 'default variables all overwrite' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
'rounded' => [
|
||||
'sm' => 'rounded-sm',
|
||||
'md' => 'rounded-md',
|
||||
'lg' => 'rounded-lg',
|
||||
],
|
||||
],
|
||||
'compounds' => [
|
||||
[
|
||||
'colors' => ['danger', 'secondary'],
|
||||
'sizes' => ['sm'],
|
||||
'class' => 'text-red-500',
|
||||
],
|
||||
],
|
||||
'defaultVariants' => [
|
||||
'colors' => 'primary',
|
||||
'sizes' => 'sm',
|
||||
'rounded' => 'md',
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'sizes' => 'sm', 'rounded' => 'lg'],
|
||||
'font-semibold border rounded text-primary text-sm rounded-lg',
|
||||
];
|
||||
|
||||
yield 'default variables without matching variants' => [
|
||||
[
|
||||
'base' => 'font-semibold border rounded',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'md' => 'text-md',
|
||||
'lg' => 'text-lg',
|
||||
],
|
||||
'rounded' => [
|
||||
'sm' => 'rounded-sm',
|
||||
'md' => 'rounded-md',
|
||||
'lg' => 'rounded-lg',
|
||||
],
|
||||
],
|
||||
'compounds' => [
|
||||
[
|
||||
'colors' => ['danger', 'secondary'],
|
||||
'sizes' => ['sm'],
|
||||
'class' => 'text-red-500',
|
||||
],
|
||||
],
|
||||
'defaultVariants' => [
|
||||
'colors' => 'primary',
|
||||
'sizes' => 'sm',
|
||||
'rounded' => 'md',
|
||||
],
|
||||
],
|
||||
[],
|
||||
'font-semibold border rounded text-primary text-sm rounded-md',
|
||||
];
|
||||
|
||||
yield 'default variables with boolean' => [
|
||||
[
|
||||
'base' => 'button',
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'blue' => 'btn-blue',
|
||||
'red' => 'btn-red',
|
||||
],
|
||||
'disabled' => [
|
||||
'true' => 'disabled',
|
||||
'false' => 'opacity-100',
|
||||
],
|
||||
],
|
||||
'defaultVariants' => [
|
||||
'colors' => 'blue',
|
||||
'disabled' => 'false',
|
||||
],
|
||||
],
|
||||
[],
|
||||
'button btn-blue opacity-100',
|
||||
];
|
||||
|
||||
yield 'boolean string variants true / true' => [
|
||||
[
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'disabled' => [
|
||||
'true' => 'disable',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'disabled' => true],
|
||||
'text-primary disable',
|
||||
];
|
||||
|
||||
yield 'boolean string variants true / false' => [
|
||||
[
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'disabled' => [
|
||||
'true' => 'disable',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'disabled' => false],
|
||||
'text-primary',
|
||||
];
|
||||
|
||||
yield 'boolean string variants false / true' => [
|
||||
[
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'disabled' => [
|
||||
'false' => 'disable',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'disabled' => true],
|
||||
'text-primary',
|
||||
];
|
||||
|
||||
yield 'boolean string variants false / false' => [
|
||||
[
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'disabled' => [
|
||||
'false' => 'disable',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'disabled' => false],
|
||||
'text-primary disable',
|
||||
];
|
||||
|
||||
yield 'boolean string variants missing' => [
|
||||
[
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'disabled' => [
|
||||
'true' => 'disable',
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary'],
|
||||
'text-primary',
|
||||
];
|
||||
|
||||
yield 'boolean list variants true' => [
|
||||
[
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'disabled' => [
|
||||
'true' => ['disable', 'opacity-50'],
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'disabled' => true],
|
||||
'text-primary disable opacity-50',
|
||||
];
|
||||
|
||||
yield 'boolean list variants false' => [
|
||||
[
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'disabled' => [
|
||||
'true' => ['disable', 'opacity-50'],
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary', 'disabled' => false],
|
||||
'text-primary',
|
||||
];
|
||||
|
||||
yield 'boolean list variants missing' => [
|
||||
[
|
||||
'variants' => [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary',
|
||||
],
|
||||
'disabled' => [
|
||||
'true' => ['disable', 'opacity-50'],
|
||||
],
|
||||
],
|
||||
],
|
||||
['colors' => 'primary'],
|
||||
'text-primary',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideAdditionalClassesCases
|
||||
*/
|
||||
public function testAdditionalClasses(string|array $base, array|string $additionals, string $expected)
|
||||
{
|
||||
$cva = new Cva($base);
|
||||
if (!$additionals) {
|
||||
$this->assertEquals($expected, $cva->apply([]));
|
||||
} else {
|
||||
$this->assertEquals($expected, $cva->apply([], ...(array) $additionals));
|
||||
}
|
||||
}
|
||||
|
||||
public static function provideAdditionalClassesCases(): iterable
|
||||
{
|
||||
yield 'additionals_are_optional' => [
|
||||
'',
|
||||
'foo',
|
||||
'foo',
|
||||
];
|
||||
|
||||
yield 'additional_are_used' => [
|
||||
'',
|
||||
'foo',
|
||||
'foo',
|
||||
];
|
||||
|
||||
yield 'additionals_are_used' => [
|
||||
'',
|
||||
['foo', 'bar'],
|
||||
'foo bar',
|
||||
];
|
||||
|
||||
yield 'additionals_preserve_order' => [
|
||||
['foo'],
|
||||
['bar', 'foo'],
|
||||
'foo bar',
|
||||
];
|
||||
|
||||
yield 'additional_are_deduplicated' => [
|
||||
'',
|
||||
['bar', 'bar'],
|
||||
'bar',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
--TEST--
|
||||
"html_cva" function
|
||||
--TEMPLATE--
|
||||
{% set alert = html_cva(
|
||||
['alert'],
|
||||
{
|
||||
color: {
|
||||
blue: 'alert-blue',
|
||||
red: 'alert-red',
|
||||
green: 'alert-green',
|
||||
yellow: 'alert-yellow',
|
||||
},
|
||||
size: {
|
||||
sm: 'alert-sm',
|
||||
md: 'alert-md',
|
||||
lg: 'alert-lg',
|
||||
},
|
||||
rounded: {
|
||||
sm: 'rounded-sm',
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
}
|
||||
},
|
||||
[{
|
||||
color: ['red'],
|
||||
size: ['lg'],
|
||||
class: 'font-semibold'
|
||||
}],
|
||||
{
|
||||
rounded: 'md'
|
||||
}
|
||||
) %}
|
||||
{{ alert.apply({color: 'blue', size: 'sm'}) }}
|
||||
--DATA--
|
||||
return []
|
||||
--EXPECT--
|
||||
alert alert-blue alert-sm rounded-md
|
||||
@@ -0,0 +1,19 @@
|
||||
--TEST--
|
||||
pass Cva object to template
|
||||
--TEMPLATE--
|
||||
{{ alert.apply({colors: 'primary', sizes: 'sm'}) }}
|
||||
--DATA--
|
||||
return [
|
||||
'alert' => new Twig\Extra\Html\Cva('font-semibold border rounded', [
|
||||
'colors' => [
|
||||
'primary' => 'text-primary',
|
||||
'secondary' => 'text-secondary'
|
||||
],
|
||||
'sizes' => [
|
||||
'sm' => 'text-sm',
|
||||
'lg' => 'text-lg'
|
||||
]
|
||||
])
|
||||
];
|
||||
--EXPECT--
|
||||
font-semibold border rounded text-primary text-sm
|
||||
Reference in New Issue
Block a user