diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index 222478cefcc0..1b38fc52e971 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -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. * diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index d061732d1ffc..b20e7c4924b8 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -31,6 +31,7 @@ class BladeCompiler extends Compiler implements CompilerInterface Concerns\CompilesLoops, Concerns\CompilesRawPhp, Concerns\CompilesStacks, + Concerns\CompilesStyles, Concerns\CompilesTranslations, ReflectsClosures; diff --git a/src/Illuminate/View/Compilers/ComponentTagCompiler.php b/src/Illuminate/View/Compilers/ComponentTagCompiler.php index 8fee31dd29a4..cf42383629ff 100644 --- a/src/Illuminate/View/Compilers/ComponentTagCompiler.php +++ b/src/Illuminate/View/Compilers/ComponentTagCompiler.php @@ -115,6 +115,10 @@ protected function compileOpeningTags(string $value) @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \)) ) | + (?: + @(?:style)(\( (?: (?>[^()]+) | (?-1) )* \)) + ) + | (?: \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\} ) @@ -176,6 +180,10 @@ protected function compileSelfClosingTags(string $value) @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \)) ) | + (?: + @(?:style)(\( (?: (?>[^()]+) | (?-1) )* \)) + ) + | (?: \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\} ) @@ -506,6 +514,10 @@ public function compileSlots(string $value) @(?:class)(\( (?: (?>[^()]+) | (?-1) )* \)) ) | + (?: + @(?:style)(\( (?: (?>[^()]+) | (?-1) )* \)) + ) + | (?: \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\} ) @@ -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 = '/ @@ -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. * diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesStyles.php b/src/Illuminate/View/Compilers/Concerns/CompilesStyles.php new file mode 100644 index 000000000000..6c715061ca4a --- /dev/null +++ b/src/Illuminate/View/Compilers/Concerns/CompilesStyles.php @@ -0,0 +1,19 @@ +\""; + } +} diff --git a/src/Illuminate/View/ComponentAttributeBag.php b/src/Illuminate/View/ComponentAttributeBag.php index 0c6c615f36c2..acf5c3fbe9f5 100644 --- a/src/Illuminate/View/ComponentAttributeBag.php +++ b/src/Illuminate/View/ComponentAttributeBag.php @@ -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. * @@ -238,9 +251,10 @@ 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) { @@ -248,6 +262,10 @@ public function merge(array $attributeDefaults = [], $escape = true) ? $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(); diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index e5ce328e0cae..47aae98576b6 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -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]; diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index 74226a488efb..01e6161af3fd 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -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(' +'); + + $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(); @@ -271,6 +280,18 @@ public function testClassDirective() 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('true])>'); + + $this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestProfileComponent', 'profile', []) +getConstructor()): ?> +except(collect(\$constructor->getParameters())->map->getName()->all()); ?> + +withAttributes(['style' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\Illuminate\Support\Arr::toCssStyles(['bar'=>true]))]); ?> @endComponentClass##END-COMPONENT-CLASS##", trim($result)); + } + public function testColonNestedComponentParsing() { $this->mockViewFactory(); diff --git a/tests/View/Blade/BladeStyleTest.php b/tests/View/Blade/BladeStyleTest.php new file mode 100644 index 000000000000..01e8c2eb14df --- /dev/null +++ b/tests/View/Blade/BladeStyleTest.php @@ -0,0 +1,14 @@ + true, 'margin-top: 10px' => false])>"; + $expected = " true, 'margin-top: 10px' => false]) ?>\">"; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } +} diff --git a/tests/View/ViewComponentAttributeBagTest.php b/tests/View/ViewComponentAttributeBagTest.php index 453014e09c84..66ce23e75866 100644 --- a/tests/View/ViewComponentAttributeBagTest.php +++ b/tests/View/ViewComponentAttributeBagTest.php @@ -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']));