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

[9.x] Add @style Blade directive #45887

Merged
merged 3 commits into from
Feb 1, 2023
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
23 changes: 23 additions & 0 deletions src/Illuminate/Collections/Arr.php
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,29 @@ public static function toCssClasses($array)
return implode(' ', $classes);
}

/**
* Conditionally compile styles from an array into a style list.
*
* @param array $array
* @return string
*/
public static function toCssStyles($array)
{
$styleList = static::wrap($array);

$styles = [];

foreach ($styleList as $class => $constraint) {
if (is_numeric($class)) {
$styles[] = Str::finish($constraint, ';');
} elseif ($constraint) {
$styles[] = Str::finish($class, ';');
}
}

return implode(' ', $styles);
}

/**
* Filter the array using the given callback.
*
Expand Down
1 change: 1 addition & 0 deletions src/Illuminate/View/Compilers/BladeCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class BladeCompiler extends Compiler implements CompilerInterface
Concerns\CompilesLoops,
Concerns\CompilesRawPhp,
Concerns\CompilesStacks,
Concerns\CompilesStyles,
Concerns\CompilesTranslations,
ReflectsClosures;

Expand Down
34 changes: 34 additions & 0 deletions src/Illuminate/View/Compilers/ComponentTagCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ protected function compileOpeningTags(string $value)
@(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
)
|
(?:
@(?:style)(\( (?: (?>[^()]+) | (?-1) )* \))
)
|
(?:
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
)
Expand Down Expand Up @@ -176,6 +180,10 @@ protected function compileSelfClosingTags(string $value)
@(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
)
|
(?:
@(?:style)(\( (?: (?>[^()]+) | (?-1) )* \))
)
|
(?:
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
)
Expand Down Expand Up @@ -506,6 +514,10 @@ public function compileSlots(string $value)
@(?:class)(\( (?: (?>[^()]+) | (?-1) )* \))
)
|
(?:
@(?:style)(\( (?: (?>[^()]+) | (?-1) )* \))
)
|
(?:
\{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
)
Expand Down Expand Up @@ -563,6 +575,7 @@ protected function getAttributesFromAttributeString(string $attributeString)
$attributeString = $this->parseShortAttributeSyntax($attributeString);
$attributeString = $this->parseAttributeBag($attributeString);
$attributeString = $this->parseComponentTagClassStatements($attributeString);
$attributeString = $this->parseComponentTagStyleStatements($attributeString);
$attributeString = $this->parseBindAttributes($attributeString);

$pattern = '/
Expand Down Expand Up @@ -665,6 +678,27 @@ protected function parseComponentTagClassStatements(string $attributeString)
);
}

/**
* Parse @style statements in a given attribute string into their fully-qualified syntax.
*
* @param string $attributeString
* @return string
*/
protected function parseComponentTagStyleStatements(string $attributeString)
{
return preg_replace_callback(
'/@(style)(\( ( (?>[^()]+) | (?2) )* \))/x', function ($match) {
if ($match[1] === 'style') {
$match[2] = str_replace('"', "'", $match[2]);

return ":style=\"\Illuminate\Support\Arr::toCssStyles{$match[2]}\"";
}

return $match[0];
}, $attributeString
);
}

/**
* Parse the "bind" attributes in a given attribute string into their fully-qualified syntax.
*
Expand Down
19 changes: 19 additions & 0 deletions src/Illuminate/View/Compilers/Concerns/CompilesStyles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Illuminate\View\Compilers\Concerns;

trait CompilesStyles
{
/**
* Compile the conditional style statement into valid PHP.
*
* @param string $expression
* @return string
*/
protected function compileStyle($expression)
{
$expression = is_null($expression) ? '([])' : $expression;

return "style=\"<?php echo \Illuminate\Support\Arr::toCssStyles{$expression} ?>\"";
}
}
24 changes: 21 additions & 3 deletions src/Illuminate/View/ComponentAttributeBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,19 @@ public function class($classList)
return $this->merge(['class' => Arr::toCssClasses($classList)]);
}

/**
* Conditionally merge styles into the attribute bag.
*
* @param mixed|array $styleList
* @return static
*/
public function style($styleList)
{
$styleList = Arr::wrap($styleList);

return $this->merge(['style' => Arr::toCssStyles($styleList)]);
}

/**
* Merge additional attributes / values into the attribute bag.
*
Expand All @@ -238,16 +251,21 @@ public function merge(array $attributeDefaults = [], $escape = true)

[$appendableAttributes, $nonAppendableAttributes] = collect($this->attributes)
->partition(function ($value, $key) use ($attributeDefaults) {
return $key === 'class' ||
(isset($attributeDefaults[$key]) &&
$attributeDefaults[$key] instanceof AppendableAttributeValue);
return $key === 'class' || $key === 'style' || (
isset($attributeDefaults[$key]) &&
$attributeDefaults[$key] instanceof AppendableAttributeValue
);
});

$attributes = $appendableAttributes->mapWithKeys(function ($value, $key) use ($attributeDefaults, $escape) {
$defaultsValue = isset($attributeDefaults[$key]) && $attributeDefaults[$key] instanceof AppendableAttributeValue
? $this->resolveAppendableAttributeDefault($attributeDefaults, $key, $escape)
: ($attributeDefaults[$key] ?? '');

if ($key === 'style') {
$value = Str::finish($value, ';');
}

return [$key => implode(' ', array_unique(array_filter([$defaultsValue, $value])))];
})->merge($nonAppendableAttributes)->all();

Expand Down
19 changes: 19 additions & 0 deletions tests/Support/SupportArrTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,25 @@ public function testToCssClasses()
$this->assertSame('font-bold mt-4 ml-2', $classes);
}

public function testToCssStyles()
{
$styles = Arr::toCssStyles([
'font-weight: bold',
'margin-top: 4px;',
]);

$this->assertSame('font-weight: bold; margin-top: 4px;', $styles);

$styles = Arr::toCssStyles([
'font-weight: bold;',
'margin-top: 4px',
'margin-left: 2px;' => true,
'margin-right: 2px' => false,
]);

$this->assertSame('font-weight: bold; margin-top: 4px; margin-left: 2px;', $styles);
}

public function testWhere()
{
$array = [100, '200', 300, '400', 500];
Expand Down
21 changes: 21 additions & 0 deletions tests/View/Blade/BladeComponentTagCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ public function testSlotsWithClassDirectiveCanBeCompiled()
$this->assertSame("@slot('foo', null, ['class' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\Illuminate\Support\Arr::toCssClasses(\$classes))]) \n".' @endslot', trim($result));
}

public function testSlotsWithStyleDirectiveCanBeCompiled()
{
$this->mockViewFactory();
$result = $this->compiler()->compileSlots('<x-slot name="foo" @style($styles)>
</x-slot>');

$this->assertSame("@slot('foo', null, ['style' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\Illuminate\Support\Arr::toCssStyles(\$styles))]) \n".' @endslot', trim($result));
}

public function testBasicComponentParsing()
{
$this->mockViewFactory();
Expand Down Expand Up @@ -271,6 +280,18 @@ public function testClassDirective()
<?php \$component->withAttributes(['class' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\Illuminate\Support\Arr::toCssClasses(['bar'=>true]))]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result));
}

public function testStyleDirective()
{
$this->mockViewFactory();
$result = $this->compiler(['profile' => TestProfileComponent::class])->compileTags('<x-profile @style(["bar"=>true])></x-profile>');

$this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', [])
<?php if (isset(\$attributes) && \$attributes instanceof Illuminate\View\ComponentAttributeBag && \$constructor = (new ReflectionClass(Illuminate\Tests\View\Blade\TestProfileComponent::class))->getConstructor()): ?>
<?php \$attributes = \$attributes->except(collect(\$constructor->getParameters())->map->getName()->all()); ?>
<?php endif; ?>
<?php \$component->withAttributes(['style' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\Illuminate\Support\Arr::toCssStyles(['bar'=>true]))]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result));
}

public function testColonNestedComponentParsing()
{
$this->mockViewFactory();
Expand Down
14 changes: 14 additions & 0 deletions tests/View/Blade/BladeStyleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Illuminate\Tests\View\Blade;

class BladeStyleTest extends AbstractBladeTestCase
{
public function testStylesAreConditionallyCompiledFromArray()
{
$string = "<span @style(['font-weight: bold', 'text-decoration: underline', 'color: red' => true, 'margin-top: 10px' => false])></span>";
$expected = "<span style=\"<?php echo \Illuminate\Support\Arr::toCssStyles(['font-weight: bold', 'text-decoration: underline', 'color: red' => true, 'margin-top: 10px' => false]) ?>\"></span>";

$this->assertEquals($expected, $this->compiler->compileString($string));
}
}
8 changes: 8 additions & 0 deletions tests/View/ViewComponentAttributeBagTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ public function testAttributeRetrieval()
$this->assertSame('class="mt-4 font-bold" name="test"', (string) $bag->class(['mt-4']));
$this->assertSame('class="mt-4 ml-2 font-bold" name="test"', (string) $bag->class(['mt-4', 'ml-2' => true, 'mr-2' => false]));

$bag = new ComponentAttributeBag(['class' => 'font-bold', 'name' => 'test', 'style' => 'margin-top: 10px']);
$this->assertSame('class="mt-4 ml-2 font-bold" style="margin-top: 10px;" name="test"', (string) $bag->class(['mt-4', 'ml-2' => true, 'mr-2' => false]));
$this->assertSame('style="margin-top: 4px; margin-left: 10px; margin-top: 10px;" class="font-bold" name="test"', (string) $bag->style(['margin-top: 4px', 'margin-left: 10px;']));

$bag = new ComponentAttributeBag(['class' => 'font-bold', 'name' => 'test', 'style' => 'margin-top: 10px; font-weight: bold']);
$this->assertSame('class="mt-4 ml-2 font-bold" style="margin-top: 10px; font-weight: bold;" name="test"', (string) $bag->class(['mt-4', 'ml-2' => true, 'mr-2' => false]));
$this->assertSame('style="margin-top: 4px; margin-left: 10px; margin-top: 10px; font-weight: bold;" class="font-bold" name="test"', (string) $bag->style(['margin-top: 4px', 'margin-left: 10px;']));

$bag = new ComponentAttributeBag([]);

$this->assertSame('class="mt-4"', (string) $bag->merge(['class' => 'mt-4']));
Expand Down