From b9c06d96d89dfe35072d52e63180cbec4c36dca7 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Wed, 18 Aug 2021 14:36:01 +0100 Subject: [PATCH] [8.x] Blade component slot attributes (#38372) * Update slot pattern * Add attributes params to existing tests * Create ComponentSlot class * Pass attributes from slot tag to Blade directive * Compile slot attributes into slot object * Add compilation tests for attribute support * Remove unused exception * Reorder arguments * Fix dynamic components with slot attributes * Escape bound attributes for slots * Update BladeComponentTagCompilerTest.php * formattinG Co-authored-by: Taylor Otwell --- .../View/Compilers/ComponentTagCompiler.php | 43 ++++++++- src/Illuminate/View/ComponentSlot.php | 89 +++++++++++++++++++ .../View/Concerns/ManagesComponents.php | 19 ++-- src/Illuminate/View/DynamicComponent.php | 2 +- .../Blade/BladeComponentTagCompilerTest.php | 20 ++++- tests/View/Blade/BladeComponentsTest.php | 2 +- tests/View/ViewFactoryTest.php | 6 +- 7 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 src/Illuminate/View/ComponentSlot.php diff --git a/src/Illuminate/View/Compilers/ComponentTagCompiler.php b/src/Illuminate/View/Compilers/ComponentTagCompiler.php index a69a704ec420..d54d297785a9 100644 --- a/src/Illuminate/View/Compilers/ComponentTagCompiler.php +++ b/src/Illuminate/View/Compilers/ComponentTagCompiler.php @@ -395,14 +395,53 @@ protected function compileClosingTags(string $value) */ public function compileSlots(string $value) { - $value = preg_replace_callback('/<\s*x[\-\:]slot\s+(:?)name=(?(\"[^\"]+\"|\\\'[^\\\']+\\\'|[^\s>]+))\s*>/', function ($matches) { + $pattern = "/ + < + \s* + x[\-\:]slot + \s+ + (:?)name=(?(\"[^\"]+\"|\\\'[^\\\']+\\\'|[^\s>]+)) + (? + (?: + \s+ + (?: + (?: + \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\} + ) + | + (?: + [\w\-:.@]+ + ( + = + (?: + \\\"[^\\\"]*\\\" + | + \'[^\']*\' + | + [^\'\\\"=<>]+ + ) + )? + ) + ) + )* + \s* + ) + (? + /x"; + + $value = preg_replace_callback($pattern, function ($matches) { $name = $this->stripQuotes($matches['name']); if ($matches[1] !== ':') { $name = "'{$name}'"; } - return " @slot({$name}) "; + $this->boundAttributes = []; + + $attributes = $this->getAttributesFromAttributeString($matches['attributes']); + + return " @slot({$name}, null, [".$this->attributesToString($attributes).']) '; }, $value); return preg_replace('/<\/\s*x[\-\:]slot[^>]*>/', ' @endslot', $value); diff --git a/src/Illuminate/View/ComponentSlot.php b/src/Illuminate/View/ComponentSlot.php new file mode 100644 index 000000000000..85665ad64575 --- /dev/null +++ b/src/Illuminate/View/ComponentSlot.php @@ -0,0 +1,89 @@ +contents = $contents; + + $this->withAttributes($attributes); + } + + /** + * Set the extra attributes that the slot should make available. + * + * @param array $attributes + * @return $this + */ + public function withAttributes(array $attributes) + { + $this->attributes = new ComponentAttributeBag($attributes); + + return $this; + } + + /** + * Get the slot's HTML string. + * + * @return string + */ + public function toHtml() + { + return $this->contents; + } + + /** + * Determine if the slot is empty. + * + * @return bool + */ + public function isEmpty() + { + return $this->contents === ''; + } + + /** + * Determine if the slot is not empty. + * + * @return bool + */ + public function isNotEmpty() + { + return ! $this->isEmpty(); + } + + /** + * Get the slot's HTML string. + * + * @return string + */ + public function __toString() + { + return $this->toHtml(); + } +} diff --git a/src/Illuminate/View/Concerns/ManagesComponents.php b/src/Illuminate/View/Concerns/ManagesComponents.php index d720e64fade4..9febac86b0c3 100644 --- a/src/Illuminate/View/Concerns/ManagesComponents.php +++ b/src/Illuminate/View/Concerns/ManagesComponents.php @@ -6,7 +6,7 @@ use Illuminate\Contracts\View\View; use Illuminate\Support\Arr; use Illuminate\Support\HtmlString; -use InvalidArgumentException; +use Illuminate\View\ComponentSlot; trait ManagesComponents { @@ -120,20 +120,17 @@ protected function componentData() * * @param string $name * @param string|null $content + * @param array $attributes * @return void - * - * @throws \InvalidArgumentException */ - public function slot($name, $content = null) + public function slot($name, $content = null, $attributes = []) { - if (func_num_args() > 2) { - throw new InvalidArgumentException('You passed too many arguments to the ['.$name.'] slot.'); - } elseif (func_num_args() === 2) { + if ($content) { $this->slots[$this->currentComponent()][$name] = $content; } elseif (ob_start()) { $this->slots[$this->currentComponent()][$name] = ''; - $this->slotStack[$this->currentComponent()][] = $name; + $this->slotStack[$this->currentComponent()][] = [$name, $attributes]; } } @@ -150,7 +147,11 @@ public function endSlot() $this->slotStack[$this->currentComponent()] ); - $this->slots[$this->currentComponent()][$currentSlot] = new HtmlString(trim(ob_get_clean())); + [$currentName, $currentAttributes] = $currentSlot; + + $this->slots[$this->currentComponent()][$currentName] = new ComponentSlot( + trim(ob_get_clean()), $currentAttributes + ); } /** diff --git a/src/Illuminate/View/DynamicComponent.php b/src/Illuminate/View/DynamicComponent.php index 23ed305a336d..cea66e77b304 100644 --- a/src/Illuminate/View/DynamicComponent.php +++ b/src/Illuminate/View/DynamicComponent.php @@ -120,7 +120,7 @@ protected function compileBindings(array $bindings) protected function compileSlots(array $slots) { return collect($slots)->map(function ($slot, $name) { - return $name === '__default' ? null : '{{ $'.$name.' }}'; + return $name === '__default' ? null : 'attributes).'>{{ $'.$name.' }}'; })->filter()->implode(PHP_EOL); } diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php index 1d65a715718d..3a5bc700ebb2 100644 --- a/tests/View/Blade/BladeComponentTagCompilerTest.php +++ b/tests/View/Blade/BladeComponentTagCompilerTest.php @@ -23,7 +23,7 @@ public function testSlotsCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot('foo') \n".' @endslot', trim($result)); + $this->assertSame("@slot('foo', null, []) \n".' @endslot', trim($result)); } public function testDynamicSlotsCanBeCompiled() @@ -31,7 +31,23 @@ public function testDynamicSlotsCanBeCompiled() $result = $this->compiler()->compileSlots(' '); - $this->assertSame("@slot(\$foo) \n".' @endslot', trim($result)); + $this->assertSame("@slot(\$foo, null, []) \n".' @endslot', trim($result)); + } + + public function testSlotsWithAttributesCanBeCompiled() + { + $result = $this->compiler()->compileSlots(' +'); + + $this->assertSame("@slot('foo', null, ['class' => 'font-bold']) \n".' @endslot', trim($result)); + } + + public function testSlotsWithDynamicAttributesCanBeCompiled() + { + $result = $this->compiler()->compileSlots(' +'); + + $this->assertSame("@slot('foo', null, ['class' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute(\$classes)]) \n".' @endslot', trim($result)); } public function testBasicComponentParsing() diff --git a/tests/View/Blade/BladeComponentsTest.php b/tests/View/Blade/BladeComponentsTest.php index 8aefd61a341f..8f7925f3e6d0 100644 --- a/tests/View/Blade/BladeComponentsTest.php +++ b/tests/View/Blade/BladeComponentsTest.php @@ -44,7 +44,7 @@ public function testEndComponentClassesAreCompiled() public function testSlotsAreCompiled() { - $this->assertSame('slot(\'foo\', ["foo" => "bar"]); ?>', $this->compiler->compileString('@slot(\'foo\', ["foo" => "bar"])')); + $this->assertSame('slot(\'foo\', null, ["foo" => "bar"]); ?>', $this->compiler->compileString('@slot(\'foo\', null, ["foo" => "bar"])')); $this->assertSame('slot(\'foo\'); ?>', $this->compiler->compileString('@slot(\'foo\')')); } diff --git a/tests/View/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index 38c857573df6..a3b2a316c2cd 100755 --- a/tests/View/ViewFactoryTest.php +++ b/tests/View/ViewFactoryTest.php @@ -355,7 +355,7 @@ public function testComponentHandling() $factory->getDispatcher()->shouldReceive('dispatch'); $factory->startComponent('component', ['name' => 'Taylor']); $factory->slot('title'); - $factory->slot('website', 'laravel.com'); + $factory->slot('website', 'laravel.com', []); echo 'title
'; $factory->endSlot(); echo 'component'; @@ -371,7 +371,7 @@ public function testComponentHandlingUsingViewObject() $factory->getDispatcher()->shouldReceive('dispatch'); $factory->startComponent($factory->make('component'), ['name' => 'Taylor']); $factory->slot('title'); - $factory->slot('website', 'laravel.com'); + $factory->slot('website', 'laravel.com', []); echo 'title
'; $factory->endSlot(); echo 'component'; @@ -392,7 +392,7 @@ public function testComponentHandlingUsingClosure() return $factory->make('component'); }, ['name' => 'Taylor']); $factory->slot('title'); - $factory->slot('website', 'laravel.com'); + $factory->slot('website', 'laravel.com', []); echo 'title
'; $factory->endSlot(); echo 'component';