diff --git a/composer.json b/composer.json index 4ad2e92..3182b14 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,11 @@ "larastan/larastan": "^2.9.1", "orchestra/testbench": "^9", "laravel/pint": "^1.14", - "rector/rector": "^1.0" + "rector/rector": "^1.0", + "livewire/livewire": "^3.4" + }, + "suggest": { + "laravel/livewire": "Required for Livewire layout support." }, "autoload": { "psr-4": { diff --git a/src/Html/Enums/LayoutPosition.php b/src/Html/Enums/LayoutPosition.php index 1025bd9..9865d04 100644 --- a/src/Html/Enums/LayoutPosition.php +++ b/src/Html/Enums/LayoutPosition.php @@ -4,8 +4,25 @@ namespace Yajra\DataTables\Html\Enums; +use Illuminate\Support\Str; + enum LayoutPosition: string { - case Start = 'Start'; - case End = 'End'; + case Top = 'top'; + case TopStart = 'topStart'; + case TopEnd = 'topEnd'; + case Bottom = 'bottom'; + case BottomStart = 'bottomStart'; + case BottomEnd = 'bottomEnd'; + + public function withOrder(?int $order): string + { + if ($order && $order > 0) { + $parts = Str::of($this->value)->ucsplit(); + + return $parts->shift().$order.$parts->first(); + } + + return $this->value; + } } diff --git a/src/Html/Layout.php b/src/Html/Layout.php index a4235be..ef4ca10 100644 --- a/src/Html/Layout.php +++ b/src/Html/Layout.php @@ -4,8 +4,14 @@ namespace Yajra\DataTables\Html; +use Illuminate\Contracts\Support\Renderable; +use Illuminate\Support\Facades\Blade; use Illuminate\Support\Fluent; use Illuminate\Support\Traits\Macroable; +use Illuminate\View\Component; +use InvalidArgumentException; +use Livewire\Livewire; +use Throwable; use Yajra\DataTables\Html\Enums\LayoutPosition; class Layout extends Fluent @@ -17,79 +23,128 @@ public static function make(array $options = []): static return new static($options); } - public function topStart(string|array|null $options, int $order = 0): static + public function top(array|string|null $options, ?int $order = null): static { - return $this->top($options, $order, LayoutPosition::Start); + $this->attributes[LayoutPosition::Top->withOrder($order)] = $options; + + return $this; } - public function top(array|string|null $options, ?int $order = null, ?LayoutPosition $position = null): static + public function topStart(string|array|null $options, ?int $order = null): static { - if ($order > 0) { - $this->attributes["top{$order}{$position?->value}"] = $options; - } else { - $this->attributes["top{$position?->value}"] = $options; - } + $this->attributes[LayoutPosition::TopStart->withOrder($order)] = $options; return $this; } - public function topEnd(string|array|null $options, int $order = 0): static + public function topEnd(string|array|null $options, ?int $order = null): static { - return $this->top($options, $order, LayoutPosition::End); + $this->attributes[LayoutPosition::TopEnd->withOrder($order)] = $options; + + return $this; } - public function topEndView(string $selector, int $order = 0): static + public function topView(string $selector, ?int $order = null): static { - return $this->topView($selector, $order, LayoutPosition::End); + return $this->top($this->renderCustomElement($selector), $order); } - public function topView(string $selector, int $order = 0, ?LayoutPosition $position = null): static + public function topStartView(string $selector, ?int $order = null): static { - $script = "function() { return $('{$selector}').html(); }"; + return $this->topStart($this->renderCustomElement($selector), $order); + } - return $this->top($script, $order, $position); + public function topEndView(string $selector, ?int $order = null): static + { + return $this->topEnd($this->renderCustomElement($selector), $order); } - public function bottomStartView(string $selector, int $order = 0): static + public function bottom(array|string|null $options, ?int $order = null): static { - return $this->bottomView($selector, $order, LayoutPosition::Start); + $this->attributes[LayoutPosition::Bottom->withOrder($order)] = $options; + + return $this; } - public function bottomView(string $selector, int $order = 0, ?LayoutPosition $position = null): static + public function bottomStart(string|array|null $options, ?int $order = null): static { - $script = "function() { return $('{$selector}').html(); }"; + $this->attributes[LayoutPosition::BottomStart->withOrder($order)] = $options; - return $this->bottom($script, $order, $position); + return $this; } - public function bottom(array|string|null $options, ?int $order = null, ?LayoutPosition $position = null): static + public function bottomEnd(string|array|null $options, ?int $order = null): static { - if ($order > 0) { - $this->attributes["bottom{$order}{$position?->value}"] = $options; - } else { - $this->attributes["bottom{$position?->value}"] = $options; - } + $this->attributes[LayoutPosition::BottomEnd->withOrder($order)] = $options; return $this; } - public function bottomEndView(string $selector, int $order = 0): static + public function bottomView(string $selector, ?int $order = null): static { - return $this->bottomView($selector, $order, LayoutPosition::End); + return $this->bottom($this->renderCustomElement($selector), $order); } - public function topStartView(string $selector, int $order = 0): static + public function bottomStartView(string $selector, ?int $order = null): static { - return $this->topView($selector, $order, LayoutPosition::Start); + return $this->bottomStart($this->renderCustomElement($selector), $order); } - public function bottomStart(string|array|null $options, int $order = 0): static + public function bottomEndView(string $selector, ?int $order = null): static { - return $this->bottom($options, $order, LayoutPosition::Start); + return $this->bottomEnd($this->renderCustomElement($selector), $order); + } + + /** + * @throws Throwable + */ + public function addView( + Component|Renderable|string $view, + LayoutPosition $layoutPosition, + ?int $order = null + ): static { + if ($view instanceof Component) { + $view = Blade::renderComponent($view); + } + + $html = $view instanceof Renderable ? $view->render() : Blade::render($view); + + $element = json_encode($html); + + if ($element === false) { + throw new InvalidArgumentException("Cannot render view [$html] to json."); + } + + $this->attributes[$layoutPosition->withOrder($order)] = $this->renderCustomElement($element, false); + + return $this; } - public function bottomEnd(string|array|null $options, int $order = 0): static + /** + * @param class-string $component + * + * @throws Throwable + */ + public function addLivewire( + string $component, + LayoutPosition $layoutPosition, + ?int $order = null + ): static { + $html = json_encode(Livewire::mount($component)); + + if ($html === false) { + throw new InvalidArgumentException("Cannot render Livewire component [$component] to json."); + } + + $this->attributes[$layoutPosition->withOrder($order)] = $this->renderCustomElement($html, false); + + return $this; + } + + private function renderCustomElement(string $element, bool $asJsSelector = true): string { - return $this->bottom($options, $order, LayoutPosition::End); + $html = $asJsSelector ? "$('{$element}').html()" : $element; + + return "function() { return $html; }"; } } diff --git a/tests/LayoutTest.php b/tests/LayoutTest.php index 3739cc7..5fed156 100644 --- a/tests/LayoutTest.php +++ b/tests/LayoutTest.php @@ -2,10 +2,15 @@ namespace Yajra\DataTables\Html\Tests; +use InvalidArgumentException; +use Livewire\Exceptions\ComponentNotFoundException; use PHPUnit\Framework\Attributes\Test; use Yajra\DataTables\Html\Builder; use Yajra\DataTables\Html\Enums\LayoutPosition; use Yajra\DataTables\Html\Layout; +use Yajra\DataTables\Html\Tests\TestComponents\TestInlineView; +use Yajra\DataTables\Html\Tests\TestComponents\TestLivewire; +use Yajra\DataTables\Html\Tests\TestComponents\TestView; class LayoutTest extends TestCase { @@ -37,16 +42,16 @@ public function it_can_set_positions(): void $layout->bottom('test', 1); $this->assertEquals('test', $layout->get('bottom1')); - $layout->top('test', 1, LayoutPosition::Start); + $layout->topStart('test', 1); $this->assertEquals('test', $layout->get('top1Start')); - $layout->bottom('test', 1, LayoutPosition::Start); + $layout->bottomStart('test', 1); $this->assertEquals('test', $layout->get('bottom1Start')); - $layout->top('test', 1, LayoutPosition::End); + $layout->topEnd('test', 1); $this->assertEquals('test', $layout->get('top1End')); - $layout->bottom('test', 1, LayoutPosition::End); + $layout->bottomEnd('test', 1); $this->assertEquals('test', $layout->get('bottom1End')); } @@ -63,10 +68,10 @@ public function it_can_be_used_in_builder(): void $layout->bottomEnd('test'); $layout->top('test', 1); $layout->bottom('test', 1); - $layout->top('test', 1, LayoutPosition::Start); - $layout->bottom('test', 1, LayoutPosition::Start); - $layout->top('test', 1, LayoutPosition::End); - $layout->bottom('test', 1, LayoutPosition::End); + $layout->topStart('test', 1); + $layout->bottomStart('test', 1); + $layout->topEnd('test', 1); + $layout->bottomEnd('test', 1); }); $this->assertArrayHasKey('layout', $builder->getAttributes()); @@ -179,4 +184,135 @@ public function it_can_accept_js_selector_for_layout_content(): void $builder->getAttributes()['layout']['bottomEnd'] ); } + + #[Test] + public function it_can_accept_view_instance_or_string_for_layout_content(): void + { + $builder = resolve(Builder::class); + + $view = view('test-view'); + + $builder->layout(fn (Layout $layout) => $layout + ->addView( + view: new TestView(), + layoutPosition: LayoutPosition::Top, + ) + ->addView( + view: new TestInlineView(), + layoutPosition: LayoutPosition::Bottom, + ) + ->addView( + view: $view, + layoutPosition: LayoutPosition::TopStart, + order: 1 + ) + ->addView( + view: 'test-view', + layoutPosition: LayoutPosition::BottomEnd, + order: 2 + ) + ->addView( + view: (new TestView())->render(), + layoutPosition: LayoutPosition::Top, + order: 3 + ) + ->addView( + view: (new TestInlineView())->render(), + layoutPosition: LayoutPosition::Bottom, + order: 4 + ) + ); + + $this->assertArrayHasKey('layout', $builder->getAttributes()); + $this->assertCount(6, $builder->getAttributes()['layout']); + + $this->assertArrayHasKey('top', $builder->getAttributes()['layout']); + $this->assertEquals( + 'function() { return '.json_encode($view->render()).'; }', + $builder->getAttributes()['layout']['top'] + ); + + $this->assertArrayHasKey('bottom', $builder->getAttributes()['layout']); + $this->assertEquals( + 'function() { return '.json_encode('

Test Inline View

').'; }', + $builder->getAttributes()['layout']['bottom'] + ); + + $this->assertArrayHasKey('top1Start', $builder->getAttributes()['layout']); + $this->assertEquals( + 'function() { return '.json_encode($view->render()).'; }', + $builder->getAttributes()['layout']['top1Start'] + ); + + $this->assertArrayHasKey('bottom2End', $builder->getAttributes()['layout']); + $this->assertEquals( + 'function() { return '.json_encode($view->render()).'; }', + $builder->getAttributes()['layout']['bottom2End'] + ); + + $this->assertArrayHasKey('top3', $builder->getAttributes()['layout']); + $this->assertEquals( + 'function() { return '.json_encode($view->render()).'; }', + $builder->getAttributes()['layout']['top3'] + ); + + $this->assertArrayHasKey('bottom4', $builder->getAttributes()['layout']); + $this->assertEquals( + 'function() { return '.json_encode('

Test Inline View

').'; }', + $builder->getAttributes()['layout']['bottom4'] + ); + } + + #[Test] + public function it_throws_an_exception_if_the_view_does_not_exist_when_adding_view(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('View [non-existent-view] not found.'); + + $builder = resolve(Builder::class); + $builder->layout(fn (Layout $layout) => $layout + ->addView( + view: 'non-existent-view', + layoutPosition: LayoutPosition::Top, + ) + ->addView( + view: view('non-existent-view'), + layoutPosition: LayoutPosition::Bottom, + )); + } + + #[Test] + public function it_can_accept_livewire_component_as_layout_content(): void + { + $builder = resolve(Builder::class); + $builder->layout(fn (Layout $layout) => $layout + ->addLivewire(TestLivewire::class, LayoutPosition::TopStart, 1) + ->addLivewire(TestLivewire::class, LayoutPosition::BottomEnd, 2)); + + $this->assertArrayHasKey('layout', $builder->getAttributes()); + $this->assertArrayHasKey('top1Start', $builder->getAttributes()['layout']); + $this->assertStringContainsString( + 'test livewire', + $builder->getAttributes()['layout']['top1Start'] + ); + + $this->assertArrayHasKey('layout', $builder->getAttributes()); + $this->assertArrayHasKey('bottom2End', $builder->getAttributes()['layout']); + $this->assertStringContainsString( + 'test livewire', + $builder->getAttributes()['layout']['bottom2End'] + ); + } + + #[Test] + public function it_throws_an_exception_if_the_livewire_component_does_not_exist_when_adding_livewire_component(): void + { + $this->expectException(ComponentNotFoundException::class); + $this->expectExceptionMessage('Unable to find component: [Yajra\DataTables\Html\Tests\TestComponents\TestView]'); + + $builder = resolve(Builder::class); + $builder->layout(fn (Layout $layout) => $layout + ->addLivewire(TestView::class, LayoutPosition::Top) + ->addLivewire(TestView::class, LayoutPosition::Bottom)); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 80b53b4..09a82ee 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,9 @@ namespace Yajra\DataTables\Html\Tests; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\View; +use Livewire\LivewireServiceProvider; use Orchestra\Testbench\TestCase as BaseTestCase; use Yajra\DataTables\DataTablesServiceProvider; use Yajra\DataTables\Html\Builder; @@ -16,8 +19,11 @@ protected function setUp(): void { parent::setUp(); + config()->set('app.key', 'base64:6pASQ5U2UYo+w6noM9hwOPeHJ5vGP+BNruyMtRA8FWY='); + $this->migrateDatabase(); $this->seedDatabase(); + $this->setupViewAndBladeDirectory(); } protected function migrateDatabase(): void @@ -104,6 +110,7 @@ protected function getEnvironmentSetUp($app): void protected function getPackageProviders($app): array { return [ + LivewireServiceProvider::class, DataTablesServiceProvider::class, HtmlServiceProvider::class, ]; @@ -113,4 +120,10 @@ protected function getHtmlBuilder(): Builder { return app(Builder::class); } + + protected function setupViewAndBladeDirectory(): void + { + View::addLocation(__DIR__.'/TestComponents'); + Blade::componentNamespace('Yajra\\DataTables\\Html\\Tests\\TestComponents', 'test'); + } } diff --git a/tests/TestComponents/TestInlineView.php b/tests/TestComponents/TestInlineView.php new file mode 100644 index 0000000..a7f419a --- /dev/null +++ b/tests/TestComponents/TestInlineView.php @@ -0,0 +1,15 @@ +Test Inline View

+ blade; + } +} diff --git a/tests/TestComponents/TestLivewire.php b/tests/TestComponents/TestLivewire.php new file mode 100644 index 0000000..36277b4 --- /dev/null +++ b/tests/TestComponents/TestLivewire.php @@ -0,0 +1,15 @@ +test livewire + blade; + } +} diff --git a/tests/TestComponents/TestView.php b/tests/TestComponents/TestView.php new file mode 100644 index 0000000..7651711 --- /dev/null +++ b/tests/TestComponents/TestView.php @@ -0,0 +1,14 @@ +Test blade file