Skip to content

Commit 3c501ee

Browse files
authored
[11.x] allow guessing of nested component (#52669)
* allow guessing of nested component sometimes components will be part of a larger component grouping, and you may wish to group those components in a folder. as a simple example we'll use the classic "Card" component, which may be organized as such: - `App\View\Components\Card\Card` - `App\View\Components\Card\Header` - `App\View\Components\Card\Body` to utilize these in your Blade file you would: ```blade <x-card.card> <x-card.header>Title</x-card.header> <x-card.body>lorem ipsum</x-card.body> </x-card.card> ``` while this is fine, it doesn't read as nice as it could. with this commit, we can now omit the trailing duplicated word, as long as the class name matches the folder name. ```blade <x-card> <x-card.header>Title</x-card.header> <x-card.body>lorem ipsum</x-card.body> </x-card> ``` we do something similar with [Anonymous components](https://laravel.com/docs/11.x/blade#anonymous-index-components) * minor formatting * add test for nested default component parsing * minor formatting * allow using folder name for anonymous index components per @taylorotwell s request, allow users to name their root components with `index.blade.php` OR the name of the component folder. * fix tests the code changes add an extra `exists()` call, so we'll adjust our numbers here. * add some tests these are copy/paste adjustments of the existing "index" tests.
1 parent a236107 commit 3c501ee

File tree

2 files changed

+128
-3
lines changed

2 files changed

+128
-3
lines changed

src/Illuminate/View/Compilers/ComponentTagCompiler.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ public function componentClass(string $component)
299299
return $class;
300300
}
301301

302+
if (class_exists($class = $class.'\\'.Str::afterLast($class, '\\'))) {
303+
return $class;
304+
}
305+
302306
if (! is_null($guess = $this->guessAnonymousComponentUsingNamespaces($viewFactory, $component)) ||
303307
! is_null($guess = $this->guessAnonymousComponentUsingPaths($viewFactory, $component))) {
304308
return $guess;
@@ -338,6 +342,7 @@ protected function guessAnonymousComponentUsingPaths(Factory $viewFactory, strin
338342
if (! is_null($guess = match (true) {
339343
$viewFactory->exists($guess = $path['prefixHash'].$delimiter.$formattedComponent) => $guess,
340344
$viewFactory->exists($guess = $path['prefixHash'].$delimiter.$formattedComponent.'.index') => $guess,
345+
$viewFactory->exists($guess = $path['prefixHash'].$delimiter.$formattedComponent.'.'.Str::afterLast($formattedComponent, '.')) => $guess,
341346
default => null,
342347
})) {
343348
return $guess;
@@ -376,6 +381,10 @@ protected function guessAnonymousComponentUsingNamespaces(Factory $viewFactory,
376381
if ($viewFactory->exists($view = $this->guessViewName($componentName, $directory).'.index')) {
377382
return $view;
378383
}
384+
385+
if ($viewFactory->exists($view = $this->guessViewName($componentName, $directory).'.'.Str::afterLast($componentName, '.'))) {
386+
return $view;
387+
}
379388
});
380389
}
381390

tests/View/Blade/BladeComponentTagCompilerTest.php

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,24 @@ public function testBasicComponentParsing()
115115
'@endComponentClass##END-COMPONENT-CLASS##</div>', trim($result));
116116
}
117117

118+
public function testNestedDefaultComponentParsing()
119+
{
120+
$container = new Container;
121+
$container->instance(Application::class, $app = m::mock(Application::class));
122+
$container->instance(Factory::class, $factory = m::mock(Factory::class));
123+
$app->shouldReceive('getNamespace')->once()->andReturn('App\\');
124+
Container::setInstance($container);
125+
126+
$result = $this->compiler()->compileTags('<div><x-card /></div>');
127+
128+
$this->assertSame("<div>##BEGIN-COMPONENT-CLASS##@component('App\View\Components\Card\Card', 'card', [])
129+
<?php if (isset(\$attributes) && \$attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
130+
<?php \$attributes = \$attributes->except(\App\View\Components\Card\Card::ignoredParameterNames()); ?>
131+
<?php endif; ?>
132+
<?php \$component->withAttributes([]); ?>\n".
133+
'@endComponentClass##END-COMPONENT-CLASS##</div>', trim($result));
134+
}
135+
118136
public function testBasicComponentWithEmptyAttributesParsing()
119137
{
120138
$this->mockViewFactory();
@@ -501,6 +519,25 @@ public function testClasslessComponentsWithIndexView()
501519
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
502520
}
503521

522+
public function testClasslessComponentsWithComponentView()
523+
{
524+
$container = new Container;
525+
$container->instance(Application::class, $app = m::mock(Application::class));
526+
$container->instance(Factory::class, $factory = m::mock(Factory::class));
527+
$app->shouldReceive('getNamespace')->andReturn('App\\');
528+
$factory->shouldReceive('exists')->andReturn(false, false, true);
529+
Container::setInstance($container);
530+
531+
$result = $this->compiler()->compileTags('<x-anonymous-component :name="\'Taylor\'" :age="31" wire:model="foo" />');
532+
533+
$this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'anonymous-component', ['view' => 'components.anonymous-component.anonymous-component','data' => ['name' => 'Taylor','age' => 31,'wire:model' => 'foo']])
534+
<?php if (isset(\$attributes) && \$attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
535+
<?php \$attributes = \$attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
536+
<?php endif; ?>
537+
<?php \$component->withAttributes(['name' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute('Taylor'),'age' => 31,'wire:model' => 'foo']); ?>\n".
538+
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
539+
}
540+
504541
public function testPackagesClasslessComponents()
505542
{
506543
$container = new Container;
@@ -528,7 +565,7 @@ public function testClasslessComponentsWithAnonymousComponentNamespace()
528565
$container->instance(Factory::class, $factory = m::mock(Factory::class));
529566

530567
$app->shouldReceive('getNamespace')->once()->andReturn('App\\');
531-
$factory->shouldReceive('exists')->times(3)->andReturnUsing(function ($arg) {
568+
$factory->shouldReceive('exists')->times(4)->andReturnUsing(function ($arg) {
532569
// In our test, we'll do as if the 'public.frontend.anonymous-component'
533570
// view exists and not the others.
534571
return $arg === 'public.frontend.anonymous-component';
@@ -562,7 +599,7 @@ public function testClasslessComponentsWithAnonymousComponentNamespaceWithIndexV
562599
$container->instance(Factory::class, $factory = m::mock(Factory::class));
563600

564601
$app->shouldReceive('getNamespace')->once()->andReturn('App\\');
565-
$factory->shouldReceive('exists')->times(4)->andReturnUsing(function (string $viewNameBeingCheckedForExistence) {
602+
$factory->shouldReceive('exists')->times(5)->andReturnUsing(function (string $viewNameBeingCheckedForExistence) {
566603
// In our test, we'll do as if the 'public.frontend.anonymous-component'
567604
// view exists and not the others.
568605
return $viewNameBeingCheckedForExistence === 'admin.auth.components.anonymous-component.index';
@@ -588,6 +625,40 @@ public function testClasslessComponentsWithAnonymousComponentNamespaceWithIndexV
588625
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
589626
}
590627

628+
public function testClasslessComponentsWithAnonymousComponentNamespaceWithComponentView()
629+
{
630+
$container = new Container;
631+
632+
$container->instance(Application::class, $app = m::mock(Application::class));
633+
$container->instance(Factory::class, $factory = m::mock(Factory::class));
634+
635+
$app->shouldReceive('getNamespace')->once()->andReturn('App\\');
636+
$factory->shouldReceive('exists')->times(6)->andReturnUsing(function (string $viewNameBeingCheckedForExistence) {
637+
// In our test, we'll do as if the 'public.frontend.anonymous-component'
638+
// view exists and not the others.
639+
return $viewNameBeingCheckedForExistence === 'admin.auth.components.anonymous-component.anonymous-component';
640+
});
641+
642+
Container::setInstance($container);
643+
644+
$blade = m::mock(BladeCompiler::class)->makePartial();
645+
646+
$blade->shouldReceive('getAnonymousComponentNamespaces')->once()->andReturn([
647+
'admin.auth' => 'admin.auth.components',
648+
]);
649+
650+
$compiler = $this->compiler([], [], $blade);
651+
652+
$result = $compiler->compileTags('<x-admin.auth::anonymous-component :name="\'Taylor\'" :age="31" wire:model="foo" />');
653+
654+
$this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'admin.auth::anonymous-component', ['view' => 'admin.auth.components.anonymous-component.anonymous-component','data' => ['name' => 'Taylor','age' => 31,'wire:model' => 'foo']])
655+
<?php if (isset(\$attributes) && \$attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
656+
<?php \$attributes = \$attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
657+
<?php endif; ?>
658+
<?php \$component->withAttributes(['name' => \Illuminate\View\Compilers\BladeCompiler::sanitizeComponentAttribute('Taylor'),'age' => 31,'wire:model' => 'foo']); ?>\n".
659+
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
660+
}
661+
591662
public function testClasslessComponentsWithAnonymousComponentPath()
592663
{
593664
$container = new Container;
@@ -621,6 +692,39 @@ public function testClasslessComponentsWithAnonymousComponentPath()
621692
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
622693
}
623694

695+
public function testClasslessComponentsWithAnonymousComponentPathComponentName()
696+
{
697+
$container = new Container;
698+
699+
$container->instance(Application::class, $app = m::mock(Application::class));
700+
$container->instance(Factory::class, $factory = m::mock(Factory::class));
701+
702+
$app->shouldReceive('getNamespace')->once()->andReturn('App\\');
703+
704+
$factory->shouldReceive('exists')->andReturnUsing(function ($arg) {
705+
return $arg === md5('test-directory').'::panel.panel';
706+
});
707+
708+
Container::setInstance($container);
709+
710+
$blade = m::mock(BladeCompiler::class)->makePartial();
711+
712+
$blade->shouldReceive('getAnonymousComponentPaths')->once()->andReturn([
713+
['path' => 'test-directory', 'prefix' => null, 'prefixHash' => md5('test-directory')],
714+
]);
715+
716+
$compiler = $this->compiler([], [], $blade);
717+
718+
$result = $compiler->compileTags('<x-panel />');
719+
720+
$this->assertSame("##BEGIN-COMPONENT-CLASS##@component('Illuminate\View\AnonymousComponent', 'panel', ['view' => '".md5('test-directory')."::panel.panel','data' => []])
721+
<?php if (isset(\$attributes) && \$attributes instanceof Illuminate\View\ComponentAttributeBag): ?>
722+
<?php \$attributes = \$attributes->except(\Illuminate\View\AnonymousComponent::ignoredParameterNames()); ?>
723+
<?php endif; ?>
724+
<?php \$component->withAttributes([]); ?>\n".
725+
'@endComponentClass##END-COMPONENT-CLASS##', trim($result));
726+
}
727+
624728
public function testClasslessIndexComponentsWithAnonymousComponentPath()
625729
{
626730
$container = new Container;
@@ -689,7 +793,7 @@ public function testItThrowsAnExceptionForNonExistingClass()
689793
$container->instance(Application::class, $app = m::mock(Application::class));
690794
$container->instance(Factory::class, $factory = m::mock(Factory::class));
691795
$app->shouldReceive('getNamespace')->once()->andReturn('App\\');
692-
$factory->shouldReceive('exists')->twice()->andReturn(false);
796+
$factory->shouldReceive('exists')->times(3)->andReturn(false);
693797
Container::setInstance($container);
694798

695799
$this->expectException(InvalidArgumentException::class);
@@ -855,3 +959,15 @@ public function render()
855959
return 'container';
856960
}
857961
}
962+
963+
namespace App\View\Components\Card;
964+
965+
use Illuminate\View\Component;
966+
967+
class Card extends Component
968+
{
969+
public function render()
970+
{
971+
return 'card';
972+
}
973+
}

0 commit comments

Comments
 (0)