Interesting, I confirm the same here too. Let me ponder on where to dig.
Laravel Blade Components: inline HTML elements wrapped with spaces
I have a Blade component which generates an HMTL link element given a page id, which needs to work as an inline element :
View file
Lorem ipsum l'<x-front.link id="12">anchor text</a>, etc...
components/front/link.blade.php
<a href="{{ route('page', $id) }}" {{ $attributes }}>{{ $slot }}</a>
Resulting HTML
Lorem ipsum l' <a href="/page/12">anchor text</a> ,etc...
The issue here are the spaces around the generated link element. I did try to have some cleanup in the component class, without success:
App/View/Components/Front/Link.php
public function render()
{
return function (array $data) {
$html = View::make('components.front.link', $data)->render();
return trim(preg_replace("#^\s+<a#m", "<a", $html));
};
}
I've been digging with XDebug and more, I understand Blade is generating php files and evaluate them in the buffer. I can't find where those additional spaces are inserted.
Event with an empty component blade file, spaces are inserted.
Anyone has a solution?
Digging deeper into the Blade compiler, I've found this :
Illuminate\View\Compilers\ComponentTagCompiler
protected function componentString(string $component, array $attributes) {
// ...
return " @component('{$class}', '{$component}', [".$this->attributesToString($parameters, $escapeBound = false).'])
<?php $component->withAttributes(['.$this->attributesToString($attributes->all(), $escapeAttributes = $class !== DynamicComponent::class).']); ?>';
}
protected function compileClosingTags(string $value)
{
return preg_replace("/<\/\s*x[-\:][\w\-\:\.]*\s*>/", ' @endcomponentClass ', $value);
}
These two methods add a space before @component and after @endcomponentClass. Removing them does not seem to break Laravel, but I'll ask over the official repository.
Certainly looks like it. I have exactly 2 spaces too. And for your use case, I would consider it an issue.
Can't see any way to report an issue on https://github.com/laravel/laravel
Maybe I don't have the rights to do it with a basic github account?
Thank you
This is a known issue and is considered normal behavior for now: https://github.com/laravel/framework/issues/34931
I have implemented my own dirty solution, which probably doesn't work with nested components. That's OK for my own use case.
App/Providers/ViewServiceProvider
We have to change the Blade compiler singleton.
namespace App\Providers;
use App\View\Compilers\BladeCompiler;
use Illuminate\View\DynamicComponent;
class ViewServiceProvider extends \Illuminate\View\ViewServiceProvider
{
/**
* Register the Blade compiler implementation.
*
* @return void
*/
public function registerBladeCompiler()
{
$this->app->singleton('blade.compiler', function ($app) {
return tap(new BladeCompiler($app['files'], $app['config']['view.compiled']), function ($blade) {
$blade->component('dynamic-component', DynamicComponent::class);
});
});
}
}
app.php
We replace the original ViewServiceProvider with our own.
// Illuminate\View\ViewServiceProvider::class,
App\Providers\ViewServiceProvider::class, // Custom ViewServiceProvider
App\View\Compilers\BladeCompiler.php
Custom BladeCompiler, may be named CustomBladeCompiler or MyBladeCompiler, I do not use this naming convention.
namespace App\View\Compilers;
class BladeCompiler extends \Illuminate\View\Compilers\BladeCompiler {
/**
* Compile the component tags.
*
* @param string $value
* @return string
*/
protected function compileComponentTags($value)
{
if (! $this->compilesComponentTags) {
return $value;
}
return (new ComponentTagCompiler(
$this->classComponentAliases, $this->classComponentNamespaces, $this
))->compile($value);
}
}
App\View\Compilers\ComponentTagCompiler.php
This is were we have our custom code, which simply removes the additional space on our inline components. We detect inline components using an empty Interface, this is just one way to do it.
namespace App\View\Compilers;
use App\View\Components\Interfaces\Inline;
use ReflectionClass;
class ComponentTagCompiler extends \Illuminate\View\Compilers\ComponentTagCompiler
{
/**
* Left trim if the component is inline
*
* @param string $component
* @param array $attributes
*
* @return string
*/
protected function componentString(string $component, array $attributes)
{
$componentString = parent::componentString($component, $attributes);
return $this->isInlineComponent($component) ? ltrim($componentString) : $componentString;
}
/**
* Right trim on the closing token if the component is inline
*
* @param string $value
*
* @return string|string[]|null
*/
protected function compileSelfClosingTags(string $value)
{
$pattern = "/
<
\s*
x[-\:]([\w\-\:\.]*)
\s*
(?<attributes>
(?:
\s+
(?:
(?:
\{\{\s*\$attributes(?:[^}]+?)?\s*\}\}
)
|
(?:
[\w\-:.@]+
(
=
(?:
\\"[^\\"]*\\"
|
\'[^\']*\'
|
[^\'\\"=<>]+
)
)?
)
)
)*
\s*
)
\/>
/x";
return preg_replace_callback($pattern, function (array $matches) {
$this->boundAttributes = [];
$attributes = $this->getAttributesFromAttributeString($matches['attributes']);
$result = $this->componentString($matches[1], $attributes) . "\n@endcomponentClass ";
return $this->isInlineComponent($matches[1]) ? rtrim($result) : $result;
}, $value);
}
/**
* Right trim if the component is inline
*
* Added a capturing group to the Regex
*
* @param string $value
*
* @return string|string[]|null
*/
protected function compileClosingTags(string $value)
{
return preg_replace_callback("/<\/\s*x[-\:]([\w\-\:\.]*)\s*>/", function (array $matches) {
return $this->isInlineComponent($matches[1]) ? ' @endcomponentClass' : ' @endcomponentClass ';
}, $value);
}
/**
* Return true if the given component implements the Inline interface
*
* @param $component
*
* @return bool
*/
protected function isInlineComponent($component)
{
$class = $this->componentClass($component);
try {
$reflection = new ReflectionClass($class);
return $reflection->implementsInterface(Inline::class);
} catch (\Exception $e) {
return false;
}
}
}
App\View\Components\Interfaces\Inline.php Empty interface to have a rather clean way to detect inline components
namespace App\View\Components\Interfaces;
/**
* Interface Inline
*
* Used by the Blade compiler to detect inline components
*
* @package App\View\Components\Interfaces
*/
interface Inline
{
}
App/View/Components/Front/Link.php
namespace App\View\Components\Front;
use App\View\Components\Interfaces\Inline;
class Link extends Component implements Inline
{
//...
}
Optional feature, allow an Inline component to change the compiler behavior. Easy to define in the Interface and implement with a Trait.
I hope this bug will be fixed in the future.
Alternative way
One could add additional rare characters around @component and @endcomponentClass, and keep the additional space. Example : return '∑∑∑ @component(...' and '@endcomponentClass ΩΩΩ'.
After the Blade compilation, one could replace these special tokens and the additional space. Pretty basic and not very efficient.
Illuminate\View\Compilers\BladeCompiler.php has a $precompilers property, which may come handy for this task.
@bplace I was thinking about doing a trim() on the components before dropping into the parent. Just a thought, I did not dig into any code.
Got bugs in my own solution, I've switched to Thomas Riccioli's solution as described on https://github.com/laravel/framework/issues/34931#issuecomment-722451972
Here is the updated ComponentTagCompiler file:
class ComponentTagCompiler extends \Illuminate\View\Compilers\ComponentTagCompiler
{
protected function componentString(string $component, array $attributes)
{
$componentString = parent::componentString($component, $attributes);
return $this->isInlineComponent($component) ? "{{''}}" . ltrim($componentString, ' ') : $componentString;
}
protected function compileSelfClosingTags(string $value)
{
// ... pattern from parent
return preg_replace_callback($pattern, function (array $matches) {
$this->boundAttributes = [];
$attributes = $this->getAttributesFromAttributeString($matches['attributes']);
$result = $this->componentString($matches[1], $attributes) . "\n@endcomponentClass ";
return $this->isInlineComponent($matches[1]) ? $result . "{{''}}" : $result;
}, $value);
}
protected function compileClosingTags(string $value)
{
return preg_replace_callback("/<\/\s*x[-\:]([\w\-\:\.]*)\s*>/", function (array $matches) {
return $this->isInlineComponent($matches[1]) ? ' @endcomponentClass' . "{{''}}" : ' @endcomponentClass ';
}, $value);
}
protected function isInlineComponent($component)
{
$class = $this->componentClass($component);
try {
$reflection = new ReflectionClass($class);
return $reflection->implementsInterface(Inline::class);
} catch (\Exception $e) {
return false;
}
}
}
Please or to participate in this conversation.