├── resources ├── views │ ├── core │ │ ├── render.blade.php │ │ ├── css.blade.php │ │ ├── dynamic-component.blade.php │ │ ├── render-in-layout.blade.php │ │ └── js.blade.php │ ├── components │ │ ├── form │ │ │ ├── checkbox.blade.php │ │ │ ├── datepicker.blade.php │ │ │ ├── input.blade.php │ │ │ ├── input-group.blade.php │ │ │ └── select.blade.php │ │ ├── loading.blade.php │ │ ├── dropdown │ │ │ ├── header.blade.php │ │ │ └── drop-down.blade.php │ │ ├── actions │ │ │ ├── drop-down.blade.php │ │ │ ├── icon.blade.php │ │ │ ├── responsive.blade.php │ │ │ └── icon-and-title.blade.php │ │ ├── toolbar │ │ │ ├── search.blade.php │ │ │ ├── bulk-actions.blade.php │ │ │ ├── filters.blade.php │ │ │ ├── toolbar.blade.php │ │ │ └── sorting.blade.php │ │ ├── link.blade.php │ │ ├── buttons │ │ │ ├── button.blade.php │ │ │ ├── icon.blade.php │ │ │ └── select.blade.php │ │ ├── filters │ │ │ ├── select-filter.blade.php │ │ │ ├── date-filter.blade.php │ │ │ └── boolean-filter.blade.php │ │ ├── img.blade.php │ │ ├── modal.blade.php │ │ ├── attributes-list.blade.php │ │ ├── icon.blade.php │ │ ├── badge.blade.php │ │ ├── alerts-handler.blade.php │ │ ├── button.blade.php │ │ ├── tooltip.blade.php │ │ ├── confirmation-message.blade.php │ │ ├── editable.blade.php │ │ ├── alert.blade.php │ │ └── table.blade.php │ ├── view │ │ └── layout.blade.php │ ├── list-view │ │ ├── list-item.blade.php │ │ └── list-view.blade.php │ ├── detail-view │ │ └── detail-view.blade.php │ ├── table-view │ │ └── table-view.blade.php │ └── grid-view │ │ ├── grid-view-item.blade.php │ │ └── grid-view.blade.php ├── scss │ ├── tailwindcss.scss │ ├── app.scss │ └── _custom-utilities.scss └── js │ ├── app.js │ └── bootstrap.js ├── .gitattributes ├── .vscode └── settings.json ├── .gitignore ├── tailwind.config.js ├── public ├── mix-manifest.json └── laravel-views.css ├── src ├── Filters │ ├── Filter.php │ ├── DateFilter.php │ ├── BooleanFilter.php │ └── BaseFilter.php ├── Facades │ ├── Header.php │ ├── Variants.php │ ├── LaravelViews.php │ └── UI.php ├── helpers.php ├── Views │ ├── ListView.php │ ├── Components │ │ └── DynamicComponent.php │ ├── TableView.php │ ├── GridView.php │ ├── View.php │ ├── Traits │ │ ├── WithSortableDropdown.php │ │ ├── WithAlerts.php │ │ └── WithActions.php │ ├── DetailView.php │ └── DataView.php ├── Actions │ ├── Confirmable.php │ ├── RedirectAction.php │ └── Action.php ├── Console │ ├── GridViewMakeCommand.php │ ├── TableViewMakeCommand.php │ ├── ListViewMakeCommand.php │ ├── BaseViewCommand.php │ ├── ActionMakeCommand.php │ ├── MakeViewCommand.php │ └── FilterMakeCommand.php ├── Data │ ├── Contracts │ │ ├── Sortable.php │ │ ├── Searchable.php │ │ └── Filterable.php │ ├── TableViewSortData.php │ ├── TableViewFilterData.php │ ├── QueryStringData.php │ └── TableViewSearchData.php ├── Macros │ ├── StrMacros.php │ └── LaravelViewsTestMacros.php ├── UI │ ├── Header.php │ ├── UI.php │ └── Variants.php ├── config │ └── laravel-views.php ├── LaravelViews.php └── LaravelViewsServiceProvider.php ├── phpcs.xml ├── tests ├── Mock │ ├── Actions │ │ ├── TestErrorAction.php │ │ ├── TestConfirmedAction.php │ │ ├── TestSuccessAction.php │ │ ├── TestDeleteUsersAction.php │ │ └── TestConfirmedDeleteUsersAction.php │ ├── MockDetailViewWithComponents.php │ ├── MockDetailViewWithMultipleComponents.php │ ├── MockListView.php │ ├── MockTableViewWithModelClass.php │ ├── MockTableView.php │ ├── MockGridView.php │ ├── MockDetailViewWithActions.php │ ├── MockTableViewWithActions.php │ ├── MockDetailView.php │ ├── MockReviewTableViewWithSearch.php │ ├── MockTableViewWithSearchAndFilters.php │ ├── MockTableViewWithDefaultFilterValue.php │ └── MockTableViewWithBulkActions.php ├── Unit │ ├── RedirectActionTest.php │ ├── ActionTest.php │ ├── HeaderTest.php │ ├── FilterTest.php │ ├── UITest.php │ ├── VariantsTest.php │ └── LaravelViewsTest.php ├── Database │ ├── ReviewTest.php │ ├── factories │ │ ├── ReviewFactory.php │ │ └── UserFactory.php │ ├── UserTest.php │ └── migrations │ │ └── 2014_10_12_000000_create_test_tables.php ├── Feature │ ├── ListViewTest.php │ ├── GridViewTest.php │ ├── TableViewTest.php │ ├── SearchDataTest.php │ ├── FilterDataTest.php │ ├── SortDataTest.php │ ├── RelationalSearchTest.php │ ├── DetailViewTest.php │ ├── ExecuteActionsTest.php │ └── BulkActionsTest.php └── TestCase.php ├── phpstan.neon.dist ├── stubs ├── detail-view.stub ├── date-filter.stub ├── list-view.stub ├── grid-view.stub ├── table-view.stub ├── action.stub ├── filter.stub ├── bulk-action.stub └── boolean-filter.stub ├── webpack.mix.js ├── CONTRIBUTING.md ├── phpunit.xml ├── LICENSE ├── package.json ├── composer.json ├── .github └── workflows │ └── run-tests.yml ├── README.md └── CHANGELOG.md /resources/views/core/render.blade.php: -------------------------------------------------------------------------------- 1 | @livewire($view) -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-detectable=false 2 | *.css linguist-detectable=false -------------------------------------------------------------------------------- /resources/scss/tailwindcss.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; -------------------------------------------------------------------------------- /resources/views/core/css.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/core/dynamic-component.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "bladeFormatter.format.indentSize": 2, 3 | "bladeFormatter.format.wrapAttributes": "force" 4 | } -------------------------------------------------------------------------------- /resources/views/components/form/checkbox.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'w-4 h-4 rounded']) }}> -------------------------------------------------------------------------------- /resources/views/core/render-in-layout.blade.php: -------------------------------------------------------------------------------- 1 | @extends($layout) 2 | 3 | @section($section) 4 | @livewire($view) 5 | @endsection -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/hot 3 | vendor 4 | .phpunit.result.cache 5 | public/laravel-views.js.LICENSE.txt 6 | **/.DS_Store 7 | -------------------------------------------------------------------------------- /resources/scss/app.scss: -------------------------------------------------------------------------------- 1 | @import "~pikaday/scss/pikaday.scss"; 2 | @import "custom-utilities"; 3 | 4 | [x-cloak] { display: none !important; } -------------------------------------------------------------------------------- /resources/views/core/js.blade.php: -------------------------------------------------------------------------------- 1 | @livewireScripts 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | }, 5 | }, 6 | variants: {}, 7 | plugins: [ 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /resources/views/components/loading.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'animate-spin']) }}> 2 | {!! UI::icon('loader', '', 'text-gray-500') !!} 3 | -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/laravel-views.js": "/laravel-views.js", 3 | "/laravel-views.css": "/laravel-views.css", 4 | "/tailwind.css": "/tailwind.css" 5 | } 6 | -------------------------------------------------------------------------------- /src/Filters/Filter.php: -------------------------------------------------------------------------------- 1 | '']) 2 | 3 |
4 | {{ __($label) }} 5 |
-------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | The PSR2 coding standard. 4 | 5 | src/ 6 | vendor 7 | -------------------------------------------------------------------------------- /resources/views/view/layout.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{-- Success/Error feedback --}} 3 | 4 | 5 | {{ $slot }} 6 | 7 | {{-- Confirmation message alert --}} 8 | 9 |
-------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * First we will load all of this project's JavaScript dependencies which 3 | * includes React and other helpers. It's a great starting point while 4 | * building robust, powerful web applications using React + Laravel. 5 | */ 6 | require('./bootstrap') -------------------------------------------------------------------------------- /src/Facades/Header.php: -------------------------------------------------------------------------------- 1 | error(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Facades/LaravelViews.php: -------------------------------------------------------------------------------- 1 | [], 'model' => null]) 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/components/toolbar/search.blade.php: -------------------------------------------------------------------------------- 1 | @if ($searchBy) 2 | @component('laravel-views::components.form.input-group', [ 3 | 'placeholder' => 'Search', 4 | 'model' => 'search', 5 | 'onClick' => 'clearSearch', 6 | 'icon' => $search ? 'x-circle' : 'search', 7 | ]) 8 | @endcomponent 9 | @endif 10 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | class(); 11 | } 12 | 13 | return $variants; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /resources/views/components/link.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.link 2 | 3 | Renders a simple link 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIRE DIRECTIVES, 5 | 6 | --}} 7 | @props(['to' => '', 'title' => '']) 8 | 9 | 10 | {{ $title }} 11 | -------------------------------------------------------------------------------- /src/Filters/DateFilter.php: -------------------------------------------------------------------------------- 1 | 'primary', 'type' => 'button']) 2 | 3 | -------------------------------------------------------------------------------- /tests/Mock/Actions/TestConfirmedAction.php: -------------------------------------------------------------------------------- 1 | success(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Mock/Actions/TestSuccessAction.php: -------------------------------------------------------------------------------- 1 | emit('test-event'); 13 | $this->success(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Views/ListView.php: -------------------------------------------------------------------------------- 1 | renderIf($model, $this)) 5 | 6 | 7 | 8 | @endif 9 | @endforeach -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/nunomaduro/larastan/extension.neon 3 | 4 | parameters: 5 | 6 | paths: 7 | - src 8 | 9 | # The level 8 is the highest level 10 | level: 5 11 | 12 | ignoreErrors: 13 | - '#Unsafe usage of new static#' 14 | 15 | excludePaths: 16 | - ./*/*/FileToBeExcluded.php 17 | 18 | checkMissingIterableValueType: false 19 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import feather from 'feather-icons' 2 | 3 | try { 4 | const setUpUiLibraries = () => { 5 | feather.replace() 6 | } 7 | 8 | document.addEventListener("DOMContentLoaded", () => { 9 | setUpUiLibraries() 10 | 11 | Livewire.hook('message.processed', () => { 12 | setUpUiLibraries() 13 | }) 14 | }) 15 | } catch (error) { 16 | throw new Error(error) 17 | } -------------------------------------------------------------------------------- /resources/views/components/buttons/icon.blade.php: -------------------------------------------------------------------------------- 1 | @props(['icon', 'size' => 'md']) 2 | 3 | -------------------------------------------------------------------------------- /src/Facades/UI.php: -------------------------------------------------------------------------------- 1 | $model->name 14 | ]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Views/Components/DynamicComponent.php: -------------------------------------------------------------------------------- 1 | view); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Mock/Actions/TestDeleteUsersAction.php: -------------------------------------------------------------------------------- 1 | delete(); 14 | $this->success(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Unit/RedirectActionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($redirectAction->id, 'redirect-action-user'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /resources/views/components/filters/select-filter.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.filters.select.blade 2 | 3 | Renders the dropdown for the select filter 4 | To customize it you should shange the UI component used, YOU SHOULD NOT CUSTOMIZE ANYHING HERE 5 | UI components used: 6 | - form/select --}} 7 | 8 | @component('laravel-views::components.form.select', [ 9 | 'name' => "filters[{$view->id}]", 10 | 'model' => "filters.{$view->id}", 11 | 'options' => array_merge(['--' => ''], $view->options()), 12 | ]) 13 | @endcomponent -------------------------------------------------------------------------------- /resources/views/components/actions/responsive.blade.php: -------------------------------------------------------------------------------- 1 | @props(['actions' => [], 'model' => null]) 2 | 3 |
4 | @if (count($actions)) 5 | {{-- Mobile actions dropdown --}} 6 |
7 | 8 |
9 | 10 | {{-- Desktop action buttons --}} 11 | 14 | @endif 15 |
-------------------------------------------------------------------------------- /resources/scss/_custom-utilities.scss: -------------------------------------------------------------------------------- 1 | .line-clamp { 2 | display: -webkit-box; 3 | -webkit-line-clamp: 1; 4 | -webkit-box-orient: vertical; 5 | overflow: hidden 6 | } 7 | 8 | .line-clamp-2 { 9 | @extend .line-clamp; 10 | -webkit-line-clamp: 2; 11 | } 12 | 13 | .line-clamp-3 { 14 | @extend .line-clamp; 15 | -webkit-line-clamp: 3; 16 | } 17 | 18 | .line-clamp-4 { 19 | @extend .line-clamp; 20 | -webkit-line-clamp: 4; 21 | } 22 | 23 | .line-clamp-5 { 24 | @extend .line-clamp; 25 | -webkit-line-clamp: 5; 26 | } 27 | -------------------------------------------------------------------------------- /tests/Mock/Actions/TestConfirmedDeleteUsersAction.php: -------------------------------------------------------------------------------- 1 | delete(); 17 | $this->success(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/views/components/filters/date-filter.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.filters.date.blade 2 | 3 | Renders the datepicker for the date filter 4 | To customize it you should shange the UI component used, YOU SHOULD NOT CUSTOMIZE ANYHING HERE 5 | UI components used: 6 | - form/datepicker --}} 7 | 8 | @component('laravel-views::components.form.datepicker', [ 9 | 'name' => "filters[{$view->id}]", 10 | 'model' => "filters.{$view->id}", 11 | 'value' => $view->selected()['selected'], 12 | 'id' => $view->getId(), 13 | ]) 14 | @endcomponent -------------------------------------------------------------------------------- /src/Console/GridViewMakeCommand.php: -------------------------------------------------------------------------------- 1 | '', 20 | 'Email' => '', 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/views/components/img.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.img 2 | 3 | Renders and image with all its variants 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIRE DIRECTIVES, 5 | it is using the variant helper to get the styles for each variant 6 | it could be img, avatar 7 | 8 | You can customize the variants classes in config/laravel-views.php 9 | 10 | props: 11 | - src 12 | - variant 13 | --}} 14 | @props(['src', 'variant' => '']) 15 | 16 | {{ $src }} -------------------------------------------------------------------------------- /resources/views/components/modal.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 10 |
-------------------------------------------------------------------------------- /resources/views/components/attributes-list.blade.php: -------------------------------------------------------------------------------- 1 | @props(['data', 'stripe' => false]) 2 | 3 | -------------------------------------------------------------------------------- /tests/Mock/MockDetailViewWithMultipleComponents.php: -------------------------------------------------------------------------------- 1 | $model->name 15 | ]), 16 | UI::attributes([ 17 | 'Email' => $model->email 18 | ]), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/views/components/icon.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.icon 2 | 3 | Renders an feather icon with its different variants 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIRE DIRECTIVES, 5 | it is using the variant helper to get the styles it could be success, danger, warning, info, default. 6 | 7 | You can customize the variants classes in config/laravel-views.php 8 | 9 | props 10 | - type 11 | - icon 12 | 13 | --}} 14 | @props(['icon', 'class']) 15 | -------------------------------------------------------------------------------- /src/Data/Contracts/Sortable.php: -------------------------------------------------------------------------------- 1 | $user->avatar, 20 | 'title' => $user->name, 21 | 'subtitle' => $user->email, 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /resources/views/list-view/list-item.blade.php: -------------------------------------------------------------------------------- 1 | @props(['avatar', 'title', 'subtitle', 'actions', 'model']) 2 | 3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 | {!! $title !!} 11 |
12 |
13 | {!! $subtitle !!} 14 |
15 |
16 | 17 |
18 |
-------------------------------------------------------------------------------- /src/Views/TableView.php: -------------------------------------------------------------------------------- 1 | headers(); 18 | 19 | return $data; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /stubs/date-filter.stub: -------------------------------------------------------------------------------- 1 | where('', $value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Database/ReviewTest.php: -------------------------------------------------------------------------------- 1 | belongsTo(UserTest::class, 'user_id', 'id'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /resources/views/components/badge.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.badge 2 | 3 | Renders a badge with its different variants 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIRE DIRECTIVES, 5 | it is using the variant helper to get the styles it could be success, danger, warning, info, default. 6 | 7 | You can customize the variants classes in config/laravel-views.php 8 | 9 | props 10 | - type 11 | - title 12 | 13 | --}} 14 | @props(['type', 'title']) 15 | 16 | 17 | 18 | {{ $title }} 19 | -------------------------------------------------------------------------------- /tests/Mock/MockTableViewWithModelClass.php: -------------------------------------------------------------------------------- 1 | name, 25 | $user->email 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Actions/RedirectAction.php: -------------------------------------------------------------------------------- 1 | title = $title; 14 | $this->icon = $icon; 15 | $this->to = $to; 16 | 17 | // Overrides the original id to create different ids for each redirect action 18 | $this->id = $this->id . '-' . $this->to; 19 | } 20 | 21 | public function handle($item) 22 | { 23 | return redirect()->route($this->to, $item); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Data/Contracts/Searchable.php: -------------------------------------------------------------------------------- 1 | '', 24 | 'title' => '', 25 | 'subtitle' => '', 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Mock/MockTableView.php: -------------------------------------------------------------------------------- 1 | name, 26 | $user->email 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/views/components/form/datepicker.blade.php: -------------------------------------------------------------------------------- 1 | {{-- table-view.date.blade 2 | 3 | Renders a datepicker input 4 | To customize it you should shange the UI component used, YOU SHOULD NOT CUSTOMIZE ANYHING HERE 5 | UI components used: 6 | - form/input 7 | props: 8 | - $label 9 | - $name 10 | - $placeholder 11 | - $value 12 | - $model 13 | - $id 14 | --}} 15 | 16 | 24 | -------------------------------------------------------------------------------- /src/Macros/StrMacros.php: -------------------------------------------------------------------------------- 1 | '', 22 | 'title' => $user->name, 23 | 'subtitle' => $user->email, 24 | 'description' => $user->email 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/views/components/actions/icon-and-title.blade.php: -------------------------------------------------------------------------------- 1 | @props(['actions', 'model' => null]) 2 | 3 | @foreach ($actions as $action) 4 | @if ($action->renderIf($model, $this)) 5 | 13 | @endif 14 | @endforeach -------------------------------------------------------------------------------- /src/Views/View.php: -------------------------------------------------------------------------------- 1 | getRenderData(), 17 | [ 18 | 'view' => $this 19 | ] 20 | ); 21 | 22 | return view("laravel-views::{$this->view}", $data); 23 | } 24 | 25 | abstract protected function getRenderData(); 26 | 27 | public function getClassName() 28 | { 29 | return $this->viewName; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /stubs/grid-view.stub: -------------------------------------------------------------------------------- 1 | '', 24 | 'title' => '', 25 | 'subtitle' => '', 26 | 'description' => '' 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/views/components/alerts-handler.blade.php: -------------------------------------------------------------------------------- 1 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |
-------------------------------------------------------------------------------- /src/Data/Contracts/Filterable.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | {{ $title }} 6 |
7 | @isset ($subtitle) 8 | {{ $subtitle }} 9 | @endisset 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 | @foreach ($components as $component) 18 |
19 | {!! $component !!} 20 |
21 | @endforeach 22 | 23 | @include('laravel-views::components.confirmation-message') 24 | -------------------------------------------------------------------------------- /src/Console/BaseViewCommand.php: -------------------------------------------------------------------------------- 1 | call('make:view', [ 29 | 'type' => $this->viewName, 30 | 'name' => $this->argument('name') 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Mock/MockDetailViewWithActions.php: -------------------------------------------------------------------------------- 1 | name, 16 | 'Detail view subtitle ' . $model->email 17 | ]; 18 | } 19 | 20 | public function detail(UserTest $model) 21 | { 22 | return [ 23 | 'Name' => $model->name, 24 | 'Email' => $model->email, 25 | 'Avatar' => $model->avatar, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/views/components/filters/boolean-filter.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.filters.boolean.blade 2 | Renders the input radius for the boolean filter 3 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIERE DIRECTIVES 4 | --}} 5 | 6 |
7 | @foreach ($view->options() as $title => $option) 8 | 18 | @endforeach 19 |
-------------------------------------------------------------------------------- /tests/Unit/ActionTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($action->shouldBeConfirmed()); 18 | 19 | $actionWithConfirmation = new class extends Action { 20 | use Confirmable; 21 | 22 | public function handle($item) {} 23 | }; 24 | 25 | $this->assertTrue($actionWithConfirmation->shouldBeConfirmed()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /stubs/table-view.stub: -------------------------------------------------------------------------------- 1 | Array of headers 18 | */ 19 | public function headers(): array 20 | { 21 | return []; 22 | } 23 | 24 | /** 25 | * Sets the data to every cell of a single row 26 | * 27 | * @param $model Current model for each row 28 | */ 29 | public function row($model): array 30 | { 31 | return []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/views/components/toolbar/bulk-actions.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if (count($selected) > 0) 3 | 4 | 5 | 6 | 7 | @endif 8 | 9 | @if ($this->hasBulkActions && isset($headers) <= 0) 10 | 16 | @endif 17 |
-------------------------------------------------------------------------------- /tests/Unit/HeaderTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($header->title, 'My header'); 14 | } 15 | 16 | public function testSetSortBy() 17 | { 18 | $header = Header::sortBy('title'); 19 | $this->assertEquals($header->sortBy, 'title'); 20 | $this->assertTrue($header->isSortable()); 21 | } 22 | 23 | public function testSetWidth() 24 | { 25 | $header = Header::width('100px'); 26 | $this->assertEquals($header->width, '100px'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/views/components/button.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.alert 2 | 3 | Renders the a button with its different variants 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIRE DIRECTIVES, 5 | it is using the variant helper to get the styles for each variant 6 | it could be primary, primary-light 7 | 8 | You can customize the variants classes in config/laravel-views.php 9 | 10 | props 11 | - title 12 | - block 13 | - variant 14 | 15 | --}} 16 | -------------------------------------------------------------------------------- /src/Data/TableViewSortData.php: -------------------------------------------------------------------------------- 1 | orderBy($field, $order); 23 | } 24 | 25 | return $query; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Mock/MockReviewTableViewWithSearch.php: -------------------------------------------------------------------------------- 1 | id, 30 | $review->user->email, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Unit/FilterTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($filter->getTitle(), 'Example Test'); 30 | $this->assertEquals($filterWithTitle->getTitle(), 'My custom title'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Views/Traits/WithSortableDropdown.php: -------------------------------------------------------------------------------- 1 | sortableBy()) instanceof Collection 17 | ? $sortableBy 18 | : collect($sortableBy); 19 | 20 | return $data; 21 | } 22 | 23 | public function sortableBy() 24 | { 25 | return []; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Feature/ListViewTest.php: -------------------------------------------------------------------------------- 1 | create(); 18 | $livewire = Livewire::test(MockListView::class); 19 | 20 | foreach ($users as $user) { 21 | $livewire->assertSee($user->name) 22 | ->assertSee($user->email) 23 | ->assertSeeHtml(htmlentities($user->avatar)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /stubs/action.stub: -------------------------------------------------------------------------------- 1 | where('active', $value); 13 | } 14 | 15 | public function options(): array 16 | { 17 | return [ 18 | 'Active' => 1, 19 | 'Disabled' => 0, 20 | ]; 21 | } 22 | } 23 | 24 | class MockTableViewWithSearchAndFilters extends MockTableView 25 | { 26 | public $searchBy = ['email']; 27 | 28 | protected function filters() 29 | { 30 | return [ 31 | new ActiveUsersFilter 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /stubs/filter.stub: -------------------------------------------------------------------------------- 1 | where('', $value); 20 | } 21 | 22 | /** 23 | * Defines the title and value for each option 24 | * 25 | * @return Array associative array with the title and values 26 | */ 27 | public function options(): Array 28 | { 29 | return []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Mock/MockTableViewWithDefaultFilterValue.php: -------------------------------------------------------------------------------- 1 | where('active', $value); 15 | } 16 | 17 | public function options(): array 18 | { 19 | return [ 20 | 'Active' => 1, 21 | 'Disabled' => 0, 22 | ]; 23 | } 24 | } 25 | 26 | class MockTableViewWithDefaultFilterValue extends MockTableView 27 | { 28 | protected function filters() 29 | { 30 | return [ 31 | new DefaultFilterValue 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /resources/views/components/form/input.blade.php: -------------------------------------------------------------------------------- 1 | {{-- table-view.select.blade 2 | 3 | Renders an input component 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIERE DIRECTIVES 5 | 6 | props: 7 | - $label 8 | - $name 9 | - $placeholder 10 | - $value 11 | - $model 12 | - $id 13 | - $attributes 14 | --}} 15 | @props(['label' => null]) 16 |
17 | @if ($label) 18 | 21 | @endif 22 | 23 | 29 |
-------------------------------------------------------------------------------- /tests/Feature/GridViewTest.php: -------------------------------------------------------------------------------- 1 | create(); 23 | 24 | $livewire = Livewire::test(MockGridView::class); 25 | 26 | foreach ($users as $user) { 27 | $livewire->assertSee($user->name) 28 | ->assertSee($user->email); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /stubs/bulk-action.stub: -------------------------------------------------------------------------------- 1 | '']) 2 | 3 |
4 | 5 | {{ $slot }} 6 | 7 | 8 |
9 |
10 | {{ $tooltip }} 11 |
12 | 13 | 14 | 15 |
16 |
-------------------------------------------------------------------------------- /resources/views/components/buttons/select.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.buttons.select 2 | 3 | Renders a button like a select dropdown. Most likely used in conjunction with the components.drop-down 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIRE DIRECTIVES, 5 | it is using the variant helper to get the styles for each variant 6 | it could be primary, primary-light 7 | 8 | You can customize the variants classes in config/laravel-views.php 9 | 10 | props 11 | - icon --}} 12 | @props(['icon' => 'chevron-down']) 13 | 21 | -------------------------------------------------------------------------------- /stubs/boolean-filter.stub: -------------------------------------------------------------------------------- 1 | where('', $value); 20 | } 21 | 22 | /** 23 | * Defines the title and value for each option 24 | * 25 | * @return Array associative array with the title and values 26 | */ 27 | public function options(): Array 28 | { 29 | return []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix') 2 | const tailwindcss = require('tailwindcss') 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Mix Asset Management 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Mix provides a clean, fluent API for defining some Webpack build steps 10 | | for your Laravel application. By default, we are compiling the Sass 11 | | file for your application, as well as bundling up your JS files. 12 | | 13 | */ 14 | 15 | mix.js('resources/js/app.js', 'public/laravel-views.js') 16 | .sass('resources/scss/app.scss', 'public/laravel-views.css') 17 | .sass('resources/scss/tailwindcss.scss', 'public/tailwind.css') 18 | .options({ 19 | processCssUrls: false, 20 | postCss: [ tailwindcss('./tailwind.config.js') ], 21 | }) 22 | .setPublicPath('public') -------------------------------------------------------------------------------- /tests/Database/factories/ReviewFactory.php: -------------------------------------------------------------------------------- 1 | define(ReviewTest::class, function (Faker $faker) { 22 | return [ 23 | 'food_id' => random_int(1, 10), 24 | 'user_id' => random_int(1, 10), 25 | 'message' => $faker->text, 26 | ]; 27 | }); 28 | -------------------------------------------------------------------------------- /tests/Mock/MockTableViewWithBulkActions.php: -------------------------------------------------------------------------------- 1 | setMessage('success', $message); 12 | } 13 | 14 | public function error($message = null) 15 | { 16 | $this->setMessage('danger', $message); 17 | } 18 | 19 | private function setMessage($type = 'success', $message = null) 20 | { 21 | $view = $this->view && $this->view instanceof View ? $this->view : $this; 22 | 23 | $messages = [ 24 | 'success' => __('Action was executed successfully'), 25 | 'danger' => __('There was an error executing this action'), 26 | ]; 27 | 28 | $view->emitSelf('notify', [ 29 | 'message' => $message ? $message : $messages[$type], 30 | 'type' => $type 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Filters/BooleanFilter.php: -------------------------------------------------------------------------------- 1 | options())->values()->toArray(); 14 | $valuesToFilter = []; 15 | 16 | foreach ($options as $option) { 17 | if (isset($values[$option]) && filter_var($values[$option], FILTER_VALIDATE_BOOLEAN)) { 18 | $valuesToFilter[$option] = true; 19 | } else { 20 | $valuesToFilter[$option] = false; 21 | } 22 | } 23 | 24 | return $valuesToFilter; 25 | } 26 | 27 | public function isChecked($option) 28 | { 29 | $values = $this->value(); 30 | 31 | return isset($values[$option]) && $values[$option]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/views/list-view/list-view.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Search input and filters --}} 3 |
4 | @include('laravel-views::components.toolbar.toolbar') 5 |
6 | 7 |
8 | @foreach ($items as $item) 9 |
10 | @if ($this->hasBulkActions) 11 |
12 | 13 |
14 | @endif 15 |
16 | 17 |
18 |
19 | @endforeach 20 |
21 | 22 | {{-- Paginator, loading indicator and totals --}} 23 |
24 | {{ $items->links() }} 25 |
26 |
27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are welcomed, feel free to open a PR with your contributions. 4 | 5 | ## Here are some points to consider: 6 | 7 | - Your PR must be making only a single change, if you want to suggest multiple features or fix multiple issues please open separate PRs. 8 | - Your PR's description should explain what the change is, what is fixing, inproving or including, if it is possible add an image or gif with the result, this will be helpful as a documentation. 9 | - Your PR must include tests if it is necessary, like with fixes or new features, besides, it must pass the Tracis CI build before it can be merged. 10 | - Your code should follows PSR-2 standard. 11 | - If you have an idea that will require a lot of work, make sure you suggest it in a new [issue](https://github.com/Gustavinho/laravel-views/issues) first to make sure it's admired before investing time into it. 12 | - Keep your code clean. Clean means you're proud of how it turned out. -------------------------------------------------------------------------------- /resources/views/components/confirmation-message.blade.php: -------------------------------------------------------------------------------- 1 |
10 |
11 | 12 |
13 |
14 | 15 | {{__("Cancel")}} 16 | 17 | 18 | {{ __("Yes, I'm sure") }} 19 | 20 | 21 | 22 |
23 |
24 |
25 |
-------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | 9 | 10 | ./tests/Unit 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /resources/views/components/toolbar/filters.blade.php: -------------------------------------------------------------------------------- 1 | {{-- Filters dropdown --}} 2 | @if (isset($filtersViews) && $filtersViews) 3 | 4 | {{-- Each filter view --}} 5 | @foreach ($filtersViews as $filter) 6 | {{-- Filter title --}} 7 | 8 |
9 | {{-- Filter view --}} 10 | @include('laravel-views::components.filters.' . $filter->view, [ 11 | 'view' => $filter, 12 | 'filter' => $filter, 13 | ]) 14 |
15 | @endforeach 16 | 17 | @if (count($filters) > 0) 18 | {{-- Clear filters button --}} 19 |
20 | 24 |
25 | @endif 26 |
27 | @endif 28 | -------------------------------------------------------------------------------- /resources/views/components/toolbar/toolbar.blade.php: -------------------------------------------------------------------------------- 1 | {{-- list-view.filters.blade 2 | 3 | Renders the search input and the filters dropdown 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIERE DIRECTIVES 5 | UI components used: 6 | - form/input-group 7 | - dropdown --}} 8 | 9 |
10 | {{-- Search input --}} 11 |
12 | @include('laravel-views::components.toolbar.search') 13 |
14 | 15 | {{-- Actions on the left --}} 16 |
17 | 18 | 19 | {{-- Bulk actions --}} 20 |
21 | @include('laravel-views::components.toolbar.bulk-actions') 22 |
23 | 24 | {{-- Sorting --}} 25 | @if (isset($sortableBy) && $sortableBy->isNotEmpty()) 26 |
27 | @include('laravel-views::components.toolbar.sorting') 28 |
29 | @endif 30 | 31 | {{-- Filters --}} 32 |
33 | @include('laravel-views::components.toolbar.filters') 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Kristijan Husak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/Database/UserTest.php: -------------------------------------------------------------------------------- 1 | 'datetime', 39 | ]; 40 | 41 | 42 | public function reviews(): \Illuminate\Database\Eloquent\Relations\HasMany 43 | { 44 | return $this->hasMany(ReviewTest::class, 'user_id', 'id'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /resources/views/components/dropdown/drop-down.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components/drop-down.blade 2 | 3 | Renders the a dropdown button with its different variants 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIRE DIRECTIVES, 5 | 6 | - slots: 7 | - trigger 8 | --}} 9 | 10 | @props(['size' => 'lg', 'dropDownWidth' => null, 'label' => '',]) 11 | 12 | @php 13 | $sizes = [ 14 | 'full' => 'full', 15 | 'sm' => 'w-48', 16 | 'lg' => 'w-64' 17 | ] 18 | @endphp 19 | 20 |
24 | 25 | @if ($label) 26 | 27 | {{ __($label) }} 28 | 29 | @else 30 | @isset($trigger) 31 | {{ $trigger }} 32 | @endisset 33 | @endif 34 | 35 | 36 |
42 | {{ $slot }} 43 |
44 |
-------------------------------------------------------------------------------- /resources/views/table-view/table-view.blade.php: -------------------------------------------------------------------------------- 1 | {{-- table-view.table-view 2 | 3 | Base layout to render all the UI componentes related to the table view, this is the main file for this view, 4 | the rest of the files are included from here 5 | 6 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIERE DIRECTIVES 7 | 8 | UI components used: 9 | - table-view.filters 10 | - components.alert 11 | - components.table 12 | - components.paginator --}} 13 | 14 | 15 | {{-- Search input and filters --}} 16 |
17 | @include('laravel-views::components.toolbar.toolbar') 18 |
19 | 20 | @if (count($items)) 21 | {{-- Content table --}} 22 |
23 | @include('laravel-views::components.table') 24 |
25 | 26 | @else 27 | {{-- Empty data message --}} 28 |
29 |

{{ __('There are no items in this table') }}

30 |
31 | @endif 32 | 33 | {{-- Paginator, loading indicator and totals --}} 34 |
35 | {{ $items->links() }} 36 |
37 |
38 | 39 | -------------------------------------------------------------------------------- /resources/views/components/form/input-group.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.form.input-group.blade 2 | 3 | Renders a input group component 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIERE DIRECTIVES 5 | 6 | props: 7 | - $label 8 | - $name 9 | - $placeholder 10 | - $value 11 | - $model 12 | - $id 13 | - $onClick 14 | - $icon 15 | --}} 16 | 17 |
18 | 21 | 32 |
33 | 34 | 35 | 36 |
37 |
-------------------------------------------------------------------------------- /src/Actions/Action.php: -------------------------------------------------------------------------------- 1 | id = $this->getId(); 33 | } 34 | 35 | public function isRedirect() 36 | { 37 | return get_class($this) === RedirectAction::class; 38 | } 39 | 40 | public function getId() 41 | { 42 | return Str::camelToDash((new \ReflectionClass($this))->getShortName()); 43 | } 44 | 45 | public function renderIf($item, View $view) 46 | { 47 | return true; 48 | } 49 | 50 | public function shouldBeConfirmed() 51 | { 52 | return method_exists($this, 'getConfirmationMessage'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Feature/TableViewTest.php: -------------------------------------------------------------------------------- 1 | create(); 19 | $livewire = Livewire::test(MockTableView::class); 20 | $headers = ['name', 'email']; 21 | 22 | foreach ($headers as $header) { 23 | $livewire->assertSee($header); 24 | } 25 | } 26 | 27 | public function testSeeAllDataOnTheTable() 28 | { 29 | $users = factory(UserTest::class, 7)->create(); 30 | 31 | Livewire::test(MockTableView::class) 32 | ->assertSeeUsers($users); 33 | } 34 | 35 | public function testSeeAllDataSettingAModelClass() 36 | { 37 | $users = factory(UserTest::class, 7)->create(); 38 | 39 | Livewire::test(MockTableViewWithModelClass::class) 40 | ->assertSeeUsers($users); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /resources/views/components/editable.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.editable 2 | 3 | Render an editable input field --}} 4 | @props(['model', 'field' => '']) 5 | 6 |
14 | 24 |
28 | {!! strip_tags($model->$field) !!} 29 |
30 | 31 |
32 | -------------------------------------------------------------------------------- /resources/views/components/form/select.blade.php: -------------------------------------------------------------------------------- 1 | {{-- table-view.select.blade 2 | 3 | Renders a select component 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIERE DIRECTIVES 5 | 6 | props: 7 | - $label 8 | - $name 9 | - $options 10 | - $selected 11 | - $model 12 | --}} 13 |
14 | @if (isset($label)) 15 | 18 | @endif 19 |
20 | 32 | 33 | {{-- Down icon --}} 34 |
35 | 36 |
37 |
38 |
-------------------------------------------------------------------------------- /tests/Feature/SearchDataTest.php: -------------------------------------------------------------------------------- 1 | assertDontSee('Search'); 20 | } 21 | 22 | public function testSeeAllDataFoundBySearchInput() 23 | { 24 | $users = factory(UserTest::class, 10)->create(); 25 | $user = $users->last(); 26 | 27 | Livewire::test(MockTableViewWithSearchAndFilters::class) 28 | ->set('search', $user->email) 29 | // Filtered user 30 | ->assertSeeUsers(collect([$user])) 31 | // Rest of the users 32 | ->assertDontSeeUsers($users->splice(0, 9)); 33 | } 34 | 35 | public function testClearSearch() 36 | { 37 | Livewire::test(MockTableViewWithSearchAndFilters::class) 38 | ->set('search', 'my-custom-search') 39 | ->call('clearSearch') 40 | ->assertSet('search', ''); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Console/ActionMakeCommand.php: -------------------------------------------------------------------------------- 1 | option('bulk'); 38 | if ($isBulk) { 39 | return __DIR__ . "/../../stubs/bulk-action.stub"; 40 | } 41 | 42 | return __DIR__ . "/../../stubs/action.stub"; 43 | } 44 | 45 | /** 46 | * Get the default namespace for the class. 47 | * 48 | * @param string $rootNamespace 49 | * 50 | * @return string 51 | */ 52 | protected function getDefaultNamespace($rootNamespace) 53 | { 54 | return $rootNamespace; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "npm run development", 8 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "watch": "npm run development -- --watch", 10 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --disable-host-check --config=node_modules/laravel-mix/setup/webpack.config.js", 11 | "prod": "npm run production", 12 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "laravel-mix": "^5.0.9", 18 | "resolve-url-loader": "^3.1.2", 19 | "sass": "^1.32.0", 20 | "sass-loader": "^8.0.2", 21 | "vue-template-compiler": "^2.6.12" 22 | }, 23 | "dependencies": { 24 | "alpinejs": "^2.8.0", 25 | "axios": "^0.19.2", 26 | "feather-icons": "^4.28.0", 27 | "jquery": "^3.5.1", 28 | "moment": "^2.29.1", 29 | "pikaday": "^1.8.2", 30 | "popper.js": "^1.16.1", 31 | "tailwindcss": "^1.9.6", 32 | "tailwindcss-plugins": "^0.3.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/UI/Header.php: -------------------------------------------------------------------------------- 1 | title = $title; 24 | 25 | return $header; 26 | } 27 | 28 | /** 29 | * Sets the sort by field 30 | * @param string $field Field the table view will be sort by 31 | * @return Header 32 | */ 33 | public function sortBy(string $field) 34 | { 35 | $this->sortBy = $field; 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * Checks if this header is sortable 42 | * @return bool 43 | * @return Header 44 | */ 45 | public function isSortable(): bool 46 | { 47 | return !empty($this->sortBy); 48 | } 49 | 50 | /** 51 | * Sets a fixed width of the column 52 | * @return Header 53 | */ 54 | public function width(string $width) 55 | { 56 | $this->width = $width; 57 | 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Database/migrations/2014_10_12_000000_create_test_tables.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->string('email'); 20 | $table->boolean('is_admin')->default(false); 21 | $table->boolean('is_writer')->default(false); 22 | $table->boolean('active')->default(true); 23 | $table->text('avatar')->nullable(); 24 | $table->rememberToken(); 25 | $table->timestamps(); 26 | }); 27 | 28 | Schema::create('reviews', function (Blueprint $table) { 29 | $table->bigIncrements('id'); 30 | $table->bigInteger('food_id'); 31 | $table->bigInteger('user_id'); 32 | $table->text('message'); 33 | $table->timestamps(); 34 | }); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | * 40 | * @return void 41 | */ 42 | public function down() 43 | { 44 | Schema::dropIfExists('users'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Console/MakeViewCommand.php: -------------------------------------------------------------------------------- 1 | argument('type'); 40 | 41 | return __DIR__ . "/../../stubs/{$view}-view.stub"; 42 | } 43 | 44 | /** 45 | * Get the default namespace for the class. 46 | * 47 | * @param string $rootNamespace 48 | * 49 | * @return string 50 | */ 51 | protected function getDefaultNamespace($rootNamespace) 52 | { 53 | return $rootNamespace . '\Http\Livewire'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Feature/FilterDataTest.php: -------------------------------------------------------------------------------- 1 | assertDontSee('Filters'); 20 | } 21 | 22 | public function testSeeAllDataFoundByAFilter() 23 | { 24 | $activeUsers = factory(UserTest::class, 5)->create(['active' => true]); 25 | $inactiveUsers = factory(UserTest::class, 5)->create(['active' => false]); 26 | 27 | Livewire::test(MockTableViewWithSearchAndFilters::class) 28 | ->set('filters', [ 29 | 'active-users-filter' => 1 30 | ]) 31 | ->assertSeeUsers($activeUsers) 32 | ->assertDontSeeUsers($inactiveUsers); 33 | } 34 | 35 | 36 | public function testClearFilters() 37 | { 38 | Livewire::test(MockTableViewWithSearchAndFilters::class) 39 | ->set('filters', [ 40 | 'active-users-filter' => 1 41 | ]) 42 | ->call('clearFilters') 43 | ->assertSet('filters', []); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Data/TableViewFilterData.php: -------------------------------------------------------------------------------- 1 | 0) { 23 | foreach ($filters as $filter) { 24 | if (isset($filterValues[$filter->id])) { 25 | 26 | /** Applies some transformation bwtween url query and filter class created */ 27 | $currentFilterValue = $filterValues[$filter->id]; 28 | $value = $filter->passValuesFromRequestToFilter($currentFilterValue); 29 | 30 | if ($value !== "") { 31 | $filter->apply($query, $value, request()); 32 | } 33 | } 34 | } 35 | } 36 | 37 | return $query; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Feature/SortDataTest.php: -------------------------------------------------------------------------------- 1 | create(); 19 | $lastUsers = factory(UserTest::class, 20)->create(); 20 | 21 | // first clic to the sortable header 22 | LiveWire::test(MockTableView::class) 23 | ->call('sort', 'id') 24 | ->assertSeeUsers($firstUsers) 25 | ->assertDontSeeUsers($lastUsers) 26 | 27 | // second clic to the sortable header 28 | ->call('sort', 'id') 29 | ->assertSeeUsers($lastUsers) 30 | ->assertDontSeeUsers($firstUsers); 31 | } 32 | 33 | public function testSeeAllDataFoundByAFilterWithADefaultValue() 34 | { 35 | $activeUsers = factory(UserTest::class, 5)->create(['active' => true]); 36 | $inactiveUsers = factory(UserTest::class, 5)->create(['active' => false]); 37 | 38 | Livewire::test(MockTableViewWithDefaultFilterValue::class) 39 | ->assertSeeUsers($activeUsers) 40 | ->assertDontSeeUsers($inactiveUsers); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /resources/views/components/toolbar/sorting.blade.php: -------------------------------------------------------------------------------- 1 | {{-- list-view.sorting.blade 2 | 3 | Renders the dropdown for the sorting select 4 | To customize it you should shange the UI component used, YOU SHOULD NOT CUSTOMIZE ANYHING HERE 5 | UI components used: 6 | - form/select --}} 7 |
8 | {{-- Sorting dropdown --}} 9 | 10 | 11 | 12 | {{ __('Sort By') }}@if ($sortableByName = $sortableBy->flip()->get($sortBy)): 13 | {{-- --}} 14 | {{ $sortableByName }} 15 | @endif 16 | 17 | 18 | 19 | 20 | 21 | {{-- Each sortable item --}} 22 | @foreach ($sortableBy as $title => $column) 23 | 28 | @if ($sortBy === $column) 29 | 30 | @else 31 | 32 | @endif 33 | {{ $title }} 34 | 35 | @endforeach 36 | 37 |
38 | -------------------------------------------------------------------------------- /resources/views/components/alert.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.alert 2 | 3 | Renders the alert message with its different variants 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIRE DIRECTIVES, 5 | it is using the variant helper to get the styles, icon and title depending of the alert type, 6 | it could be success, error, warning. 7 | 8 | You can customize the variants classes in config/laravel-views.php 9 | --}} 10 | @props(['type' => 'success', 'onClose' => '']) 11 | 12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 | {{ variants()->alert($type)->title() }}! 23 | 24 |
25 | {{ $slot }} 26 |
27 |
28 | 29 | {{-- Flush this message from the session --}} 30 | 33 |
34 |
-------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-views/laravel-views", 3 | "description": "Laravel package to create beautiful common views like tables using only PHP code", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Gustavo Martinez", 9 | "email": "gustavinho.martinez@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "livewire/livewire": "2.*" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "LaravelViews\\": "src", 18 | "LaravelViews\\Test\\": "tests" 19 | } 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "9.5.*", 23 | "laravel/legacy-factories": "^1.1", 24 | "nunomaduro/larastan": "^0.7.6", 25 | "orchestra/testbench": "^7.0" 26 | }, 27 | "scripts": { 28 | "test": "./vendor/bin/phpunit", 29 | "test:verbose": "composer run test -- --testdox", 30 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 31 | "larastan": "./vendor/bin/phpstan analyse" 32 | }, 33 | "extra": { 34 | "laravel": { 35 | "providers": [ 36 | "LaravelViews\\LaravelViewsServiceProvider" 37 | ], 38 | "aliases": { 39 | "LaravelViews": "LaravelViews\\Facades\\LaravelViews", 40 | "Variants": "LaravelViews\\Facades\\Variants", 41 | "UI": "LaravelViews\\Facades\\UI" 42 | } 43 | } 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true 47 | } 48 | -------------------------------------------------------------------------------- /src/Data/QueryStringData.php: -------------------------------------------------------------------------------- 1 | request = $request; 14 | } 15 | 16 | public function getSearchValue($currentValue) 17 | { 18 | return $this->request->query('search', $currentValue); 19 | } 20 | 21 | /** 22 | * Casts all boolean values of the querystring from string to boolean 23 | * this is needed to set the boolean filter values properly 24 | */ 25 | public function getFilterValues($currentValue) 26 | { 27 | $filters = $this->request->query('filters', $currentValue); 28 | 29 | return collect($filters)->map(function ($filter) { 30 | /** If is an array that means it came from a boolean filter */ 31 | if (is_array($filter)) { 32 | foreach ($filter as $option => $checked) { 33 | /** Casts from string to boolean */ 34 | $filter[$option] = filter_var($checked, FILTER_VALIDATE_BOOLEAN); 35 | } 36 | } 37 | 38 | return $filter; 39 | })->toArray(); 40 | } 41 | 42 | /** 43 | * Get a value from the query string 44 | * @param string $field query param name 45 | * @param string $currentValue Default value 46 | */ 47 | public function getValue($field, $currentValue) 48 | { 49 | return $this->request->query($field, $currentValue); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Console/FilterMakeCommand.php: -------------------------------------------------------------------------------- 1 | option('type'); 47 | $stub = 'filter'; 48 | 49 | if ($type == 'boolean') { 50 | $stub = 'boolean-filter'; 51 | } 52 | 53 | if ($type == 'date') { 54 | $stub = 'date-filter'; 55 | } 56 | 57 | return __DIR__ . "/../../stubs/{$stub}.stub"; 58 | } 59 | 60 | /** 61 | * Get the default namespace for the class. 62 | * 63 | * @param string $rootNamespace 64 | * 65 | * @return string 66 | */ 67 | protected function getDefaultNamespace($rootNamespace) 68 | { 69 | return $rootNamespace; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Filters/BaseFilter.php: -------------------------------------------------------------------------------- 1 | id = $this->getId(); 16 | } 17 | 18 | public function selected() 19 | { 20 | $request = request(); 21 | $selected = ''; 22 | 23 | if ($request->has('filters')) { 24 | $filters = $request->get('filters'); 25 | if (isset($filters[$this->id])) { 26 | $value = $filters[$this->id]; 27 | if ($value != "" && $value != null) { 28 | $selected = $value; 29 | if (is_array($selected)) { 30 | foreach ($selected as $key => $value) { 31 | $selected[$key] = filter_var($value, FILTER_VALIDATE_BOOLEAN); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | return [ 39 | 'selected' => $selected 40 | ]; 41 | } 42 | 43 | public function value() 44 | { 45 | return $this->selected()['selected']; 46 | } 47 | 48 | /** 49 | * Get filter title 50 | */ 51 | public function getTitle() 52 | { 53 | if (!$this->title) { 54 | return Str::classNameAsSentence((new \ReflectionClass($this))->getShortName()); 55 | } 56 | 57 | return $this->title; 58 | } 59 | 60 | public function getId() 61 | { 62 | return Str::camelToDash((new \ReflectionClass($this))->getShortName()); 63 | } 64 | 65 | public function passValuesFromRequestToFilter($values) 66 | { 67 | return $values; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/UI/UI.php: -------------------------------------------------------------------------------- 1 | component('laravel-views::components.editable', [ 12 | 'model' => $model, 13 | 'field' => $field 14 | ]); 15 | } 16 | 17 | public function badge($title, $type = 'default') 18 | { 19 | return $this->component('laravel-views::components.badge', [ 20 | 'title' => $title, 21 | 'type' => $type 22 | ]); 23 | } 24 | 25 | public function avatar($src) 26 | { 27 | return $this->component('laravel-views::components.img', [ 28 | 'src' => $src, 29 | 'variant' => 'avatar' 30 | ]); 31 | } 32 | 33 | public function link($title, $to) 34 | { 35 | return $this->component('laravel-views::components.link', compact( 36 | 'to', 37 | 'title' 38 | )); 39 | } 40 | 41 | public function icon($icon, $type = 'default', $class = "") 42 | { 43 | return $this->component('laravel-views::components.icon', compact( 44 | 'icon', 45 | 'type', 46 | 'class' 47 | )); 48 | } 49 | 50 | public function attributes($attributes, $options = []) 51 | { 52 | return $this->component('laravel-views::components.attributes-list', array_merge( 53 | ['data' => $attributes], 54 | $options 55 | )); 56 | } 57 | 58 | public function component($view, $data = []) 59 | { 60 | return View::make('laravel-views::core.dynamic-component') 61 | ->with([ 62 | 'view' => $view, 63 | 'data' => $data, 64 | ]) 65 | ->render(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /resources/views/grid-view/grid-view-item.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'image' => '', 3 | 'title' => '', 4 | 'subtitle' => '', 5 | 'description' => '', 6 | 'withBackground' => false, 7 | 'model', 8 | 'actions' => [], 9 | 'hasDefaultAction' => false, 10 | 'selected' => false 11 | ]) 12 | 13 |
14 | @if ($hasDefaultAction) 15 | 16 | {{ $image }} 17 | 18 | @else 19 | {{ $image }} 20 | @endif 21 | 22 |
23 |
24 |
25 |

26 | @if ($hasDefaultAction) 27 | 28 | {!! $title !!} 29 | 30 | @else 31 | {!! $title !!} 32 | @endif 33 |

34 | @if ($subtitle) 35 | 36 | {!! $subtitle !!} 37 | 38 | @endif 39 |
40 | 41 | @if (count($actions)) 42 |
43 | 44 |
45 | @endif 46 |
47 | 48 | @if (isset($description)) 49 |

50 | {!! $description !!} 51 |

52 | @endif 53 |
54 | 55 |
-------------------------------------------------------------------------------- /resources/views/grid-view/grid-view.blade.php: -------------------------------------------------------------------------------- 1 | {{-- grid-view.grid-view 2 | 3 | Base layout to render all the UI componentes related to the grid view, this is the main file for this view, 4 | the rest of the files are included from here 5 | 6 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIERE DIRECTIVES 7 | 8 | --}} 9 | @php 10 | // Mapped this tailwindcss utilities so they can be prged 11 | $cols = [ 12 | 1 => 'xl:grid-cols-1', 13 | 2 => 'xl:grid-cols-2', 14 | 3 => 'xl:grid-cols-3', 15 | 4 => 'xl:grid-cols-4', 16 | 5 => 'xl:grid-cols-5', 17 | 6 => 'xl:grid-cols-6', 18 | 7 => 'xl:grid-cols-7', 19 | 8 => 'xl:grid-cols-8', 20 | 9 => 'xl:grid-cols-9', 21 | 10 => 'xl:grid-cols-10', 22 | 11 => 'xl:grid-cols-11', 23 | 12 => 'xl:grid-cols-12', 24 | ] 25 | @endphp 26 | 27 | {{-- Search input and filters --}} 28 |
29 | @include('laravel-views::components.toolbar.toolbar') 30 |
31 | 32 |
33 | @foreach ($items as $item) 34 |
35 | @if ($this->hasBulkActions) 36 |
37 | 38 |
39 | @endif 40 | 50 |
51 | @endforeach 52 |
53 | 54 | {{-- Paginator, loading indicator and totals --}} 55 |
56 | {{ $items->links() }} 57 |
58 |
59 | -------------------------------------------------------------------------------- /src/Macros/LaravelViewsTestMacros.php: -------------------------------------------------------------------------------- 1 | assertEmitted('notify', [ 12 | 'message' => $message ?? __('Action was executed successfully'), 13 | 'type' => 'success' 14 | ]); 15 | 16 | return $this; 17 | }); 18 | 19 | TestableLivewire::macro('assertShowErrorAlert', function ($message = null) { 20 | $this->assertEmitted('notify', [ 21 | 'message' => $message ?? __('There was an error executing this action'), 22 | 'type' => 'danger' 23 | ]); 24 | 25 | return $this; 26 | }); 27 | 28 | TestableLivewire::macro('executeAction', function ($actionClass, $model = null) { 29 | $action = new $actionClass; 30 | $id = $model ? (is_numeric($model) ? $model : $model->getKey()) : null; 31 | $this->call('executeAction', $action->getId(), $id); 32 | 33 | return $this; 34 | }); 35 | 36 | TestableLivewire::macro('confirmAction', function ($actionClass, $model = null) { 37 | $action = new $actionClass; 38 | $id = $model ? is_numeric($model) ? $model : $model->getKey() : null; 39 | $this->call('confirmAndExecuteAction', $action->getId(), $id); 40 | 41 | return $this; 42 | }); 43 | 44 | TestableLivewire::macro('executeBulkAction', function ($actionClass) { 45 | $action = new $actionClass; 46 | $this->call('executeBulkAction', $action->getid()); 47 | 48 | return $this; 49 | }); 50 | 51 | TestableLivewire::macro('selectAll', function () { 52 | $this->set('allSelected', true); 53 | 54 | return $this; 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__ . '/Database/migrations'); 23 | $this->withFactories(__DIR__.'/Database/factories'); 24 | 25 | /** 26 | * Macro to check if a user is present in the HTML code 27 | * @example $livewire->assertSeeUsers($users) 28 | */ 29 | TestableLivewire::macro('assertSeeUsers', function ($users, $assert = 'assertSee') { 30 | foreach ($users as $user) { 31 | $this->$assert(htmlspecialchars_decode($user->name)) 32 | ->$assert($user->email); 33 | } 34 | 35 | return $this; 36 | }); 37 | 38 | /** 39 | * Macro to check if a user is not present in the HTML code 40 | * @example $livewire->assertDontSeeUsers($users) 41 | */ 42 | TestableLivewire::macro('assertDontSeeUsers', function ($users) { 43 | return TestableLivewire::assertSeeUsers($users, 'assertDontSee'); 44 | }); 45 | } 46 | 47 | protected function getPackageProviders($app) 48 | { 49 | return [ 50 | // \Spatie\LaravelRay\RayServiceProvider::class, 51 | TranslationServiceProvider::class, 52 | LivewireServiceProvider::class, 53 | LaravelViewsServiceProvider::class, 54 | ]; 55 | } 56 | 57 | protected function getPackageAliases($app) 58 | { 59 | return [ 60 | 'UI' => UI::class 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Feature/RelationalSearchTest.php: -------------------------------------------------------------------------------- 1 | users = factory(UserTest::class, 10) 26 | ->create() 27 | ->each(function (UserTest $user) { 28 | $user->reviews()->saveMany(factory(ReviewTest::class, 3)->make()); 29 | }); 30 | 31 | $this->reviews = ReviewTest::all(); 32 | } 33 | 34 | 35 | public function testSeeAllDataFoundBySearchInput() 36 | { 37 | $user = $this->reviews->last()->user; 38 | 39 | $this->livewire = Livewire::test(MockReviewTableViewWithSearch::class); 40 | 41 | $this->livewire->set('search', $user->email); 42 | 43 | $userReviews = $user->reviews; 44 | 45 | $otherUserReviews = ReviewTest::where('user_id', '!=', $user->id)->get(); 46 | 47 | // Filtered user 48 | $this->assertSeeReviews($this->livewire, $userReviews) 49 | // Rest of the users 50 | ->assertDontSeeReviews($this->livewire, $otherUserReviews); 51 | } 52 | 53 | 54 | private function assertSeeReviews($livewire, $reviews, $assert = 'assertSee') 55 | { 56 | foreach ($reviews as $review) { 57 | $livewire 58 | ->$assert($review->user->email); 59 | } 60 | 61 | return $this; 62 | } 63 | 64 | private function assertDontSeeReviews($livewire, $reviews) 65 | { 66 | return $this->assertSeeReviews($livewire, $reviews, 'assertDontsee'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - release/** 9 | - feature/** 10 | - bug/** 11 | - tests/** 12 | pull_request: 13 | branches: 14 | - master 15 | - dev 16 | - /^release-.*$/ 17 | - /^release/.*$/ 18 | - /^feature/.*$/ 19 | - /^tests/.*$/ 20 | 21 | jobs: 22 | test: 23 | runs-on: ${{ matrix.os }} 24 | strategy: 25 | fail-fast: true 26 | matrix: 27 | os: [ubuntu-latest] 28 | php: [7.3, 7.4, 8.0, 8.1] 29 | laravel: [7.*, 8.*, 9.*] 30 | stability: [prefer-stable] 31 | include: 32 | - laravel: 9.* 33 | testbench: ^7.0 34 | - laravel: 8.* 35 | testbench: ^6.6 36 | - laravel: 7.* 37 | testbench: ^5.0 38 | exclude: 39 | - laravel: 7.* 40 | php: 8.1 41 | - laravel: 9.* 42 | php: 7.3 43 | - laravel: 9.* 44 | php: 7.4 45 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Testbench ${{ matrix.testbench }} 46 | 47 | steps: 48 | - name: Checkout code 49 | uses: actions/checkout@v2 50 | 51 | - name: Setup PHP 52 | uses: shivammathur/setup-php@v2 53 | with: 54 | php-version: ${{ matrix.php }} 55 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 56 | coverage: none 57 | 58 | - name: Setup problem matchers 59 | run: | 60 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 61 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 62 | 63 | - name: Install dependencies 64 | run: | 65 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 66 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 67 | 68 | - name: Execute tests 69 | run: vendor/bin/phpunit 70 | -------------------------------------------------------------------------------- /tests/Feature/DetailViewTest.php: -------------------------------------------------------------------------------- 1 | create(); 20 | 21 | Livewire::test(MockDetailView::class, ['model' => $user->id]) 22 | ->assertSee($user->name); 23 | } 24 | 25 | public function testSeeAllDataWithAFieldListAsDefault() 26 | { 27 | $user = factory(UserTest::class)->create(); 28 | 29 | Livewire::test(MockDetailView::class, ['model' => $user]) 30 | ->assertSee($user->name) 31 | ->assertSee($user->email) 32 | ->assertSeeHtml($user->avatar) 33 | ->assertSee('Name') 34 | ->assertSee('Email') 35 | ->assertSee('Avatar'); 36 | } 37 | 38 | public function testSeeAllDatawithACustomComponent() 39 | { 40 | $user = factory(UserTest::class)->create(); 41 | 42 | Livewire::test(MockDetailViewWithComponents::class, ['model' => $user]) 43 | ->assertSeeHtml($user->name); 44 | } 45 | 46 | public function testSeeAllDataUsingMultipleComponents() 47 | { 48 | $user = factory(UserTest::class)->create(); 49 | 50 | Livewire::test(MockDetailViewWithMultipleComponents::class, ['model' => $user]) 51 | ->assertSeeHtml($user->email) 52 | ->assertSeeHtml($user->name); 53 | } 54 | 55 | public function testSeeHeading() 56 | { 57 | $user = factory(UserTest::class)->create(); 58 | 59 | Livewire::test(MockDetailView::class, ['model' => $user->id]) 60 | ->assertSee('Detail view title ' . $user->name) 61 | ->assertSee('Detail view subtitle ' . $user->email); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/config/laravel-views.php: -------------------------------------------------------------------------------- 1 | [ 10 | "light" => "hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 active:bg-gray-100 active:text-gray-900", 11 | "primary" => "text-white bg-blue-600 hover:bg-blue-700 focus:ring-blue-500", 12 | "danger" => "text-white bg-red-600 hover:bg-red-700 focus:ring-red-500", 13 | "white" => "hover:bg-gray-100 focus:ring-gray-200 border border-gray-400", 14 | "primary-light" => "text-blue-700 border border-blue-600 hover:bg-blue-600 hover:text-white focus:bg-blue-600 focus:text-white active:bg-blue-600 active:text-white", 15 | ], 16 | 17 | "alerts" => [ 18 | "success" => [ 19 | "base" => "bg-green-100 border-green-300 text-green-700", 20 | "icon" => "bg-green-200", 21 | "title" => "text-green-900", 22 | ], 23 | "danger" => [ 24 | "base" => "bg-red-100 border-red-300 text-red-700", 25 | "icon" => "bg-red-200", 26 | "title" => "text-red-900", 27 | ], 28 | "warning" => [ 29 | "base" => "bg-green-100 border-green-300 text-green-700", 30 | "icon" => "bg-green-200", 31 | "title" => "text-green-900", 32 | ] 33 | ], 34 | 35 | "badges" => [ 36 | 'success' => 'bg-green-200 text-green-800', 37 | 'danger' => 'bg-red-200 text-red-800', 38 | 'warning' => 'bg-yellow-200 text-yellow-800', 39 | 'info' => 'bg-blue-200 text-blue-800', 40 | 'default' => 'bg-gray-200 text-gray-800' 41 | ], 42 | 43 | "images" => [ 44 | 'avatar' => 'h-8 w-8 object-cover rounded-full shadow-inner' 45 | ], 46 | 47 | "icons" => [ 48 | 'success' => 'text-green-500', 49 | 'danger' => 'text-red-500', 50 | 'warning' => 'text-yellow-500', 51 | 'info' => 'text-blue-500', 52 | ], 53 | 54 | 'links' => [ 55 | 'default' => 'hover:text-blue-600 hover:underline' 56 | ], 57 | 58 | 'gridView' => [ 59 | 'selected' => 'border-2 border-blue-500' 60 | ] 61 | ]; 62 | -------------------------------------------------------------------------------- /tests/Database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(UserTest::class, function (Faker $faker) { 21 | return [ 22 | 'name' => str_replace("'", "", $faker->name), 23 | 'email' => $faker->unique()->safeEmail, 24 | 'is_admin' => $faker->randomElement([true, false]), 25 | 'is_writer' => $faker->randomElement([true, false]), 26 | 'active' => $faker->randomElement([true, false]), 27 | 'avatar' => $faker->randomElement([ 28 | 'https://gravatar.com/avatar/70d54a7e7b8a5d91e0ae87b99ccb3d61?s=400&d=robohash&r=x', 29 | 'https://gravatar.com/avatar/5d913d0e61f6ad19b61b27d1bc350363?s=400&d=robohash&r=x', 30 | // 'https://robohash.org/b842dace7186de3c8fc52ac691b585a7?set=set4&bgset=&size=400x400', 31 | 'https://gravatar.com/avatar/bc8f81437562a9def3992885573bdec5?s=400&d=robohash&r=x', 32 | 'https://gravatar.com/avatar/50746d27c0ecedf70fa5c3934cfe4ead?s=400&d=robohash&r=x', 33 | 'https://gravatar.com/avatar/a7ecb9ab617ef620ee3b8ade1d695f90?s=400&d=robohash&r=pg', 34 | 'https://gravatar.com/avatar/6b661cba4ce7d0038f6c2e5881005e49?s=400&d=robohash&r=pg', 35 | 'https://gravatar.com/avatar/94f1d9a644c6d5b36c9aec515fdaa5d1?s=400&d=robohash&r=pg', 36 | 'https://gravatar.com/avatar/619bb911835caa313806aba5ad2e13cc?s=400&d=robohash&r=pg', 37 | 'https://gravatar.com/avatar/d4c58fa6c7485d5f4b74d74a37779757?s=400&d=robohash&r=pg', 38 | 'https://gravatar.com/avatar/02313d0fa22f1f507630b6a4c32e251f?s=400&d=robohash&r=pg' 39 | ]) 40 | ]; 41 | }); 42 | -------------------------------------------------------------------------------- /src/Data/TableViewSearchData.php: -------------------------------------------------------------------------------- 1 | where(function ($query) use ($value, $regularFields, $relationalFields) { 30 | $this->applyRegularFields($regularFields, $query, $value); 31 | $this->applyRelationalFields($relationalFields, $query, $value); 32 | }); 33 | } 34 | 35 | return $query; 36 | } 37 | 38 | /** 39 | * @param $relationalFields 40 | * @param $query 41 | * @param String $value 42 | */ 43 | private static function applyRelationalFields($relationalFields, $query, string $value): void 44 | { 45 | foreach ($relationalFields as $relationalValue) { 46 | [$relationship, $field] = explode('.', $relationalValue); 47 | 48 | $query->orWhereHas($relationship, static function ($query) use ($value, $field) { 49 | $query->where($field, 'like', "%{$value}%"); 50 | }); 51 | } 52 | } 53 | 54 | /** 55 | * @param array $regularFields 56 | * @param $query 57 | * @param String $value 58 | */ 59 | private function applyRegularFields(array $regularFields, $query, string $value): void 60 | { 61 | foreach ($regularFields as $field) { 62 | $query->orWhere($field, 'like', "%{$value}%"); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Unit/UITest.php: -------------------------------------------------------------------------------- 1 | 15 | active 16 | '; 17 | 18 | $this->assertHtmlEquals($badge, $expected); 19 | } 20 | 21 | public function testBagdeSuccesstHelper() 22 | { 23 | $badge = UI::badge('active', 'success'); 24 | $expected = ' 25 | active 26 | '; 27 | 28 | $this->assertHtmlEquals($badge, $expected); 29 | } 30 | 31 | public function testAvatarHelper() 32 | { 33 | $avatar = UI::avatar('my-avatar-url'); 34 | $expected = 'my-avatar-url'; 35 | 36 | $this->assertHtmlEquals($avatar, $expected); 37 | } 38 | 39 | public function testLinkHelper() 40 | { 41 | $link = UI::link('title', '/'); 42 | $expected = ' 43 | title 44 | '; 45 | 46 | $this->assertHtmlEquals($link, $expected); 47 | } 48 | 49 | public function testDefaultIconHelper() 50 | { 51 | $icon = UI::icon('activity'); 52 | $expected = ''; 53 | 54 | $this->assertHtmlEquals($icon, $expected); 55 | } 56 | 57 | public function testIconHelperWithVariant() 58 | { 59 | $icon = UI::icon('activity', 'success'); 60 | $expected = ''; 61 | 62 | $this->assertHtmlEquals($icon, $expected); 63 | } 64 | 65 | private function assertHtmlEquals($html, $expected) 66 | { 67 | $this->assertEquals( 68 | preg_replace('/^\s+|\n|\r|\s+$/m', '', $html), 69 | preg_replace('/^\s+|\n|\r|\s+$/m', '', $expected) 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Views/DetailView.php: -------------------------------------------------------------------------------- 1 | setModel(); 27 | $this->setHeading(); 28 | } 29 | 30 | protected function getRenderData() 31 | { 32 | $detailData = app()->call([$this, 'detail'], [ 33 | 'model' => $this->model, 34 | ]); 35 | 36 | if (is_array($detailData)) { 37 | // If there is an array of data insted of a component 38 | // then creates a new attributes component 39 | if (Arr::isAssoc($detailData)) { 40 | if ($this->detailComponent) { 41 | $components = [UI::component($this->detailComponent, $detailData)]; 42 | } else { 43 | $components = [UI::attributes($detailData, ['stripe' => $this->stripe])]; 44 | } 45 | } else { 46 | $components = $detailData; 47 | } 48 | // If there is only one component 49 | } else { 50 | $components = [$detailData]; 51 | } 52 | 53 | return [ 54 | 'components' => $components, 55 | ]; 56 | } 57 | 58 | private function setModel() 59 | { 60 | if (is_numeric($this->model)) { 61 | if (!$this->modelClass) { 62 | throw new Exception('A $modelClass should be declared when the initial model value is an ID'); 63 | } 64 | 65 | $this->model = $this->modelClass::find($this->model); 66 | } 67 | } 68 | 69 | private function setHeading() 70 | { 71 | if (method_exists($this, 'heading')) { 72 | $heading = app()->call([$this, 'heading'], ['model' => $this->model]); 73 | [$this->title, $this->subtitle] = $heading; 74 | } 75 | 76 | if (!$this->title) { 77 | $this->title = $this->getClassName(); 78 | } 79 | } 80 | 81 | public function getActions() 82 | { 83 | if (method_exists($this, 'actions')) { 84 | return $this->actions(); 85 | } 86 | 87 | return []; 88 | } 89 | 90 | public function getModelWhoFiredAction() 91 | { 92 | return $this->model; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Unit/VariantsTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(UIVariants::class, variants()); 14 | } 15 | 16 | public function testButtonVariants() 17 | { 18 | $this->assertTrue( 19 | Variants::button('primary')->class() === 20 | 'text-white bg-blue-600 hover:bg-blue-700 focus:ring-blue-500' 21 | ); 22 | $this->assertEquals( 23 | Variants::button('primary-light')->class(), 24 | 'text-blue-700 border border-blue-600 hover:bg-blue-600 hover:text-white focus:bg-blue-600 focus:text-white active:bg-blue-600 active:text-white' 25 | ); 26 | } 27 | 28 | public function testAlertVariants() 29 | { 30 | $this->assertEquals( 31 | Variants::alert('success')->class('base'), 32 | 'bg-green-100 border-green-300 text-green-700' 33 | ); 34 | $this->assertEquals( 35 | Variants::alert('danger')->class('icon'), 36 | 'bg-red-200' 37 | ); 38 | $this->assertEquals( 39 | Variants::alert('warning')->class('title'), 40 | 'text-green-900' 41 | ); 42 | $this->assertEquals( 43 | Variants::alert('success')->title(), 44 | 'Success' 45 | ); 46 | $this->assertEquals( 47 | Variants::alert('danger')->icon(), 48 | 'x' 49 | ); 50 | } 51 | 52 | public function testBadgeVariants() 53 | { 54 | $this->assertEquals( 55 | Variants::badge('success')->class(), 56 | 'bg-green-200 text-green-800' 57 | ); 58 | } 59 | 60 | public function testImgVariants() 61 | { 62 | $this->assertEquals( 63 | Variants::img('avatar')->class(), 64 | 'h-8 w-8 object-cover rounded-full shadow-inner' 65 | ); 66 | $this->assertEquals( 67 | Variants::img()->class(), 68 | '' 69 | ); 70 | } 71 | 72 | public function testVarianstHelperUsingVariantPaths() 73 | { 74 | $this->assertEquals( 75 | variants('images.avatar'), 76 | 'h-8 w-8 object-cover rounded-full shadow-inner' 77 | ); 78 | 79 | $this->assertEquals( 80 | variants('alerts.warning.title'), 81 | 'text-green-900' 82 | ); 83 | 84 | $this->assertInstanceOf( 85 | UIVariants::class, 86 | variants() 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Feature/ExecuteActionsTest.php: -------------------------------------------------------------------------------- 1 | create(); 20 | 21 | Livewire::test(MockTableViewWithActions::class) 22 | ->call('executeAction', 'test-success-action', 1, true) 23 | ->assertShowSuccessAlert(); 24 | } 25 | 26 | public function testSeeErrorAlert() 27 | { 28 | factory(UserTest::class)->create(); 29 | 30 | Livewire::test(MockTableViewWithActions::class) 31 | ->call('executeAction', 'test-error-action', 1, true) 32 | ->assertShowErrorAlert(); 33 | } 34 | 35 | // TODO: Test custom error message 36 | 37 | public function testSeeMultipleRedirectActions() 38 | { 39 | factory(UserTest::class)->create(); 40 | $icons = ['eye', 'pencil']; 41 | 42 | $livewire = Livewire::test(MockTableViewWithActions::class); 43 | 44 | foreach ($icons as $icon) { 45 | $livewire->assertSeeHtml('data-feather="' . $icon . '"'); 46 | } 47 | } 48 | 49 | public function testSeeConfirmationMessage() 50 | { 51 | $user = factory(UserTest::class)->create(); 52 | $message = 'Do you really want to perform this action?'; 53 | 54 | Livewire::test(MockTableViewWithActions::class) 55 | ->executeAction(TestConfirmedAction::class, $user) 56 | ->assertEmitted('openConfirmationModal', [ 57 | 'message' => $message, 58 | 'id' => 'test-confirmed-action', 59 | 'modelId' => $user->id 60 | ]); 61 | } 62 | 63 | // TODO: test custom confirmation message 64 | 65 | public function testCallActionAfterConfirmationMessage() 66 | { 67 | $user = factory(UserTest::class)->create(); 68 | 69 | Livewire::test(MockTableViewWithActions::class) 70 | ->executeAction(TestConfirmedAction::class, $user) 71 | ->assertEmitted('openConfirmationModal', [ 72 | 'message' => 'Do you really want to perform this action?', 73 | 'id' => 'test-confirmed-action', 74 | 'modelId' => $user->id 75 | ]) 76 | ->confirmAction(TestConfirmedAction::class, $user) 77 | ->assertShowSuccessAlert(); 78 | } 79 | 80 | public function testEmitedEventFromAction() 81 | { 82 | Livewire::test(MockTableViewWithActions::class) 83 | ->executeAction(TestSuccessAction::class, 1) 84 | ->assertEmitted('test-event'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /resources/views/components/table.blade.php: -------------------------------------------------------------------------------- 1 | {{-- components.table 2 | 3 | Renders a data table 4 | You can customize all the html and css classes but YOU MUST KEEP THE BLADE AND LIVEWIRE DIRECTIVES, 5 | 6 | props: 7 | - headers 8 | - itmes 9 | - actionsByRow --}} 10 | 11 | 12 | 13 | 14 | 15 | @if ($this->hasBulkActions) 16 | 21 | @endif 22 | {{-- Renders all the headers --}} 23 | @foreach ($headers as $header) 24 | 43 | @endforeach 44 | 45 | {{-- This is a empty cell just in case there are action rows --}} 46 | @if (count($actionsByRow) > 0) 47 | 48 | @endif 49 | 50 | 51 | 52 | 53 | @foreach ($items as $item) 54 | 55 | @if ($this->hasBulkActions) 56 | 61 | @endif 62 | {{-- Renders all the content cells --}} 63 | @foreach ($view->row($item) as $column) 64 | 67 | @endforeach 68 | 69 | {{-- Renders all the actions row --}} 70 | @if (count($actionsByRow) > 0) 71 | 76 | @endif 77 | 78 | @endforeach 79 | 80 |
17 | 18 | 19 | 20 | width) ? 'width=' . $header->width . '' : '' }}> 25 | @if (is_string($header)) 26 | {{ $header }} 27 | @else 28 | @if ($header->isSortable()) 29 | 38 | @else 39 | {{ $header->title }} 40 | @endif 41 | @endif 42 |
57 | 58 | 59 | 60 | 65 | {!! $column !!} 66 | 72 |
73 | 74 |
75 |
81 | -------------------------------------------------------------------------------- /tests/Feature/BulkActionsTest.php: -------------------------------------------------------------------------------- 1 | create(); 21 | $input = ''; 22 | 23 | Livewire::test(MockTableViewWithActions::class) 24 | ->assertDontSeeHtml($input); 25 | 26 | Livewire::test(MockTableViewWithBulkActions::class) 27 | ->assertSeeHtml($input); 28 | } 29 | 30 | public function testSeeActionsButtonWhenThereAreSelectedRows() 31 | { 32 | $user = factory(UserTest::class)->create(); 33 | 34 | Livewire::test(MockTableViewWithBulkActions::class) 35 | ->assertDontSee('Actions') 36 | ->set('selected', [$user->id]) 37 | ->assertSee('Actions'); 38 | } 39 | 40 | public function testSelectUnselectAll() 41 | { 42 | $users = factory(UserTest::class, 7)->create(); 43 | 44 | Livewire::test(MockTableViewWithBulkActions::class) 45 | ->selectAll() 46 | ->assertSet('selected', $users->pluck('id')->toArray()) 47 | ->set('allSelected', false) 48 | ->assertSet('selected', []); 49 | } 50 | 51 | public function testExecuteActionToSelectedRows() 52 | { 53 | $users = factory(UserTest::class, 7)->create(); 54 | 55 | Livewire::test(MockTableViewWithBulkActions::class) 56 | ->selectAll() 57 | ->executeBulkAction(TestDeleteUsersAction::class) 58 | ->assertShowSuccessAlert() 59 | ->assertDontSeeUsers($users); 60 | 61 | foreach ($users as $user) { 62 | $this->assertDatabaseMissing('users', $user->toArray()); 63 | } 64 | } 65 | 66 | public function testExecuteBulkActionsWithConfirmationMessage() 67 | { 68 | $users = factory(UserTest::class, 7)->create(); 69 | 70 | Livewire::test(MockTableViewWithBulkActions::class) 71 | ->selectAll() 72 | ->executeBulkAction(TestConfirmedDeleteUsersAction::class) 73 | ->assertEmitted('openConfirmationModal', [ 74 | 'message' => 'Do you really want to perform this action?', 75 | 'id' => 'test-confirmed-delete-users-action', 76 | ]) 77 | ->confirmAction(TestConfirmedDeleteUsersAction::class) 78 | ->assertShowSuccessAlert() 79 | ->assertDontSeeUsers($users); 80 | 81 | foreach ($users as $user) { 82 | $this->assertDatabaseMissing('users', $user->toArray()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Unit/LaravelViewsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 15 | $css, 16 | implode("\n", [ 17 | \Livewire\Livewire::styles(), 18 | '', 19 | '', 20 | ]) 21 | ); 22 | } 23 | 24 | public function testRenderJsLinks() 25 | { 26 | $js = LaravelViews::js(); 27 | $this->assertEquals( 28 | $js, 29 | \Livewire\Livewire::scripts().PHP_EOL. 30 | ''.PHP_EOL. 31 | ''.PHP_EOL. 32 | ''.PHP_EOL. 33 | '' 34 | ); 35 | } 36 | 37 | public function testRenderCustomizedCssLinks() 38 | { 39 | $css = LaravelViews::css('laravel-views,livewire'); 40 | 41 | $this->assertEquals( 42 | $css, 43 | \Livewire\Livewire::styles()."\n". 44 | '' 45 | ); 46 | } 47 | 48 | public function testRenderCustomizedJsLinks() 49 | { 50 | $js = LaravelViews::js('laravel-views'); 51 | 52 | $this->assertEquals( 53 | $js, 54 | ''.PHP_EOL. 55 | ''.PHP_EOL. 56 | '' 57 | ); 58 | 59 | $js = LaravelViews::js('laravel-views,alpine'); 60 | 61 | $this->assertEquals( 62 | $js, 63 | ''.PHP_EOL. 64 | ''.PHP_EOL. 65 | ''.PHP_EOL. 66 | '' 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/laravel-views.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";/*! 2 | * Pikaday 3 | * Copyright © 2014 David Bushell | BSD & MIT license | https://dbushell.com/ 4 | */.pika-single{z-index:9999;display:block;position:relative;color:#333;background:#fff;border:1px solid;border-color:#ccc #ccc #bbb;font-family:Helvetica Neue,Helvetica,Arial,sans-serif}.pika-single.is-hidden{display:none}.pika-single.is-bound{position:absolute;box-shadow:0 5px 15px -5px rgba(0,0,0,.5)}.pika-single:after,.pika-single:before{content:" ";display:table}.pika-single:after{clear:both}.pika-lendar{float:left;width:240px;margin:8px}.pika-title{position:relative;text-align:center}.pika-title select{cursor:pointer;position:absolute;z-index:9998;margin:0;left:0;top:5px;opacity:0}.pika-label{display:inline-block;position:relative;z-index:9999;overflow:hidden;margin:0;padding:5px 3px;font-size:14px;line-height:20px;font-weight:700;color:#333;background-color:#fff}.pika-next,.pika-prev{display:block;cursor:pointer;position:relative;outline:none;border:0;padding:0;width:20px;height:30px;text-indent:20px;white-space:nowrap;overflow:hidden;background-color:transparent;background-position:50%;background-repeat:no-repeat;background-size:75% 75%;opacity:.5}.pika-next:hover,.pika-prev:hover{opacity:1}.pika-next.is-disabled,.pika-prev.is-disabled{cursor:default;opacity:.2}.is-rtl .pika-next,.pika-prev{float:left;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==")}.is-rtl .pika-prev,.pika-next{float:right;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=")}.pika-select{display:inline-block}.pika-table{width:100%;border-collapse:collapse;border-spacing:0;border:0}.pika-table td,.pika-table th{width:14.2857142857%;padding:0}.pika-table th{color:#999;font-size:12px;line-height:25px;font-weight:700;text-align:center}.pika-table abbr{border-bottom:none;cursor:help}.pika-button{cursor:pointer;display:block;box-sizing:border-box;outline:none;border:0;margin:0;width:100%;padding:5px;color:#666;font-size:12px;line-height:15px;text-align:right;background:#f5f5f5;height:auto}.has-event .pika-button{color:#fff;background:#3af}.is-today .pika-button{color:#3af;font-weight:700}.is-selected .pika-button{color:#fff;font-weight:700;background:#3af;box-shadow:inset 0 1px 3px #178fe5;border-radius:3px}.is-disabled .pika-button,.is-outside-current-month .pika-button{color:#999;opacity:.3}.is-disabled .pika-button{pointer-events:none;cursor:default}.pika-button:hover{color:#fff;background:#ff8000;box-shadow:none;border-radius:3px}.pika-button .is-selection-disabled{pointer-events:none;cursor:default}.pika-week{font-size:11px;color:#999}.is-inrange .pika-button{color:#666;background:#d5e9f7}.is-startrange .pika-button{color:#fff;background:#6cb31d;box-shadow:none;border-radius:3px}.is-endrange .pika-button{color:#fff;background:#3af;box-shadow:none;border-radius:3px}.line-clamp,.line-clamp-2,.line-clamp-3,.line-clamp-4,.line-clamp-5{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-3{-webkit-line-clamp:3}.line-clamp-4{-webkit-line-clamp:4}.line-clamp-5{-webkit-line-clamp:5}[x-cloak]{display:none!important} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Laravel views](https://laravelviews.com/img/docs/table.png) 2 | 3 | # Laravel views 4 | 5 | Laravel package to create beautiful common views like data tables using the [TALL stack](https://tallstack.dev/). 6 | 7 | # Documentation 8 | Read the [full documentation](https://laravelviews.com) 9 | 10 | # Live examples 11 | See some [live examples](https://laravelviews.com/examples/table-view) for the different views. 12 | 13 | ## Contributing 14 | 15 | Check the [contribution guide](CONTRIBUTING.md) 16 | 17 | ## Roadmap 18 | 19 | Laravel Views is still under heavy development so I will be adding more awesome features and views. 20 | 21 | Here's the plan for what's coming: 22 | 23 | - **New form view** 24 | - **New layout view** 25 | - Add a download action 26 | - Add translations 27 | - Add links as a UI helpers 28 | 29 | ## Upgrade guide 30 | ### From 2.4.0 to 2.4.1 31 | **Publish blade componentes** 32 | 33 | Some of the internal components have changed, if you have published these components before to customize them, you will not have them up to date, unfourtunately you need to publish them again with `php artisan vendor:publish --tag=views --provider='LaravelViews\LaravelViewsServiceProvider'` and customize them as you need. 34 | 35 | ### From 2.2 to 2.3 36 | **Cached views** 37 | 38 | 39 | The blade directives have changed, you need to clear the cached views with `php artisan view:clear` 40 | 41 | **Public assets** 42 | 43 | The main assets (JS and CSS files) have changed, you need to publish the public assets again with `php artisan vendor:publish --tag=public --provider='LaravelViews\LaravelViewsServiceProvider' --force` 44 | 45 | **Publish blade componentes** 46 | 47 | Some of the internal components have changed, if you have published these components before to customize them, you will not have them up to date, unfourtunately you need to publish them again with `php artisan vendor:publish --tag=views --provider='LaravelViews\LaravelViewsServiceProvider'` and customize them as you need. 48 | 49 | **Method `renderIf()` in actions** 50 | 51 | Update the renderIf() function in your action classes adding a new `$view` parameter as follows: 52 | ```php 53 | shouldVerifyConfirmation = true; 19 | $this->executeActionHandler($actionId, $actionableItemId); 20 | } 21 | 22 | public function confirmAndExecuteAction($actionId, $actionableItemId) 23 | { 24 | $this->shouldVerifyConfirmation = false; 25 | $this->executeActionHandler($actionId, $actionableItemId); 26 | } 27 | 28 | public function executeBulkAction($action) 29 | { 30 | $this->shouldVerifyConfirmation = true; 31 | $this->executeActionHandler($action); 32 | } 33 | 34 | public function confirmAndExecuteBulkAction($action) 35 | { 36 | $this->shouldVerifyConfirmation = false; 37 | $this->executeActionHandler($action); 38 | } 39 | 40 | private function executeActionHandler($actionId, $actionableItemId = null) 41 | { 42 | /** @var Action */ 43 | $action = $this->findAction($actionId); 44 | 45 | if ($action) { 46 | // If the action needs confirmation 47 | if ($this->shouldVerifyConfirmation && $action->shouldBeConfirmed()) { 48 | $this->confirmAction($action, $actionableItemId); 49 | } else { 50 | // If $actionableItemId is null then it is a bulk action 51 | // and it uses the current selection 52 | $actionableItems = $actionableItemId ? $this->getModelWhoFiredAction($actionableItemId) : $this->selected; 53 | $action->view = $this; 54 | $action->handle($actionableItems, $this); 55 | } 56 | } else { 57 | throw new Exception("Unable to find the {$actionId} action"); 58 | } 59 | } 60 | 61 | /** 62 | * Opens confirmation modal for a specific action 63 | * @param Action $action 64 | */ 65 | private function confirmAction($action, $modelId = null) 66 | { 67 | $actionData = [ 68 | 'message' => $action->getConfirmationMessage($modelId ? $this->getModelWhoFiredAction($modelId) : null), 69 | 'id' => $action->getId() 70 | ]; 71 | 72 | if ($modelId) { 73 | $actionData['modelId'] = $modelId; 74 | } 75 | $this->emitSelf('openConfirmationModal', $actionData); 76 | } 77 | 78 | /** 79 | * Finds an action by its id 80 | */ 81 | private function findAction(string $actionId) 82 | { 83 | $actions = collect($this->actions)->merge($this->bulkActions); 84 | 85 | return $actions->first( 86 | function ($actionToFind) use ($actionId) { 87 | return $actionToFind->id === $actionId; 88 | } 89 | ); 90 | } 91 | 92 | /** 93 | * Computed properties 94 | */ 95 | public function getActionsProperty() 96 | { 97 | // This `getActions()` function needs to be defined by the 98 | // view that is using actions 99 | return $this->getActions(); 100 | } 101 | 102 | public function getBulkActionsProperty() 103 | { 104 | if (method_exists($this, 'bulkActions')) { 105 | return $this->bulkActions(); 106 | } 107 | 108 | return []; 109 | } 110 | 111 | public function getHasBulkActionsProperty() 112 | { 113 | return method_exists($this, 'bulkActions') && count($this->bulkActions) > 0; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/UI/Variants.php: -------------------------------------------------------------------------------- 1 | path = $path; 18 | } 19 | 20 | /** 21 | * Uses the button component 22 | * 23 | * @param String $variant component variant 24 | */ 25 | public function button($variant = '') 26 | { 27 | $this->component = 'buttons'; 28 | $this->variant = $variant; 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * Uses the alert component 35 | * 36 | * @param String $variant component variant 37 | */ 38 | public function alert($variant = '') 39 | { 40 | $this->component = 'alerts'; 41 | $this->variant = $variant; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Uses the paginator component 48 | * 49 | * @param String $variant component variant 50 | */ 51 | public function paginator($variant = '') 52 | { 53 | $this->component = 'paginator'; 54 | $this->variant = $variant; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Uses the badge component 61 | * 62 | * @param String $variant component variant 63 | */ 64 | public function badge($variant = '') 65 | { 66 | $this->component = 'badges'; 67 | $this->variant = $variant; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Uses the img component 74 | * 75 | * @param String $variant component variant 76 | */ 77 | public function img($variant = '') 78 | { 79 | $this->component = 'images'; 80 | $this->variant = $variant; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Uses the icon component 87 | * 88 | * @param String $variant component variant 89 | */ 90 | public function featherIcon($variant = '') 91 | { 92 | $this->component = 'icons'; 93 | $this->variant = $variant; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Get the class string for the component and variant selected 100 | * 101 | * @param String $element Set the internal element of the component if there are any 102 | */ 103 | public function class($element = '') 104 | { 105 | // Returns the config path directly it it was set in the constructor 106 | if ($this->path) { 107 | return config("laravel-views.{$this->path}"); 108 | } else { 109 | $config = "laravel-views.{$this->component}.{$this->variant}"; 110 | if ($element) { 111 | return config("{$config}.{$element}"); 112 | } 113 | 114 | return config($config); 115 | } 116 | } 117 | 118 | /** 119 | * Get the title of the variant and componente selected 120 | */ 121 | public function title() 122 | { 123 | $titles = [ 124 | 'alerts' => [ 125 | 'success' => 'Success', 126 | 'danger' => 'Error', 127 | 'warning' => 'Warning', 128 | ] 129 | ]; 130 | 131 | return $titles[$this->component][$this->variant]; 132 | } 133 | 134 | /** 135 | * Get the icon of the variant and componente selected 136 | */ 137 | public function icon() 138 | { 139 | $icons = [ 140 | 'alerts' => [ 141 | 'success' => 'check', 142 | 'danger' => 'x', 143 | 'warning' => 'alert-circle', 144 | ] 145 | ]; 146 | 147 | return $icons[$this->component][$this->variant]; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/LaravelViews.php: -------------------------------------------------------------------------------- 1 | name 17 | return [ 18 | 'buttons.icon' => 'icon-button', 19 | 'buttons.select' => 'select-button', 20 | 'buttons.button' => 'button', 21 | 'dropdown.drop-down' => 'drop-down', 22 | 'dropdown.header' => 'drop-down.header', 23 | 'actions.responsive' => 'actions', 24 | 'actions.drop-down' => 'actions.drop-down', 25 | 'actions.icon-and-title' => 'actions.icon-and-title', 26 | 'actions.icon' => 'actions.icon', 27 | 'attributes-list' => 'attributes-list', 28 | 'alert' => 'alert', 29 | 'alerts-handler' => 'alerts-handler', 30 | 'form.input-group' => 'form.input-group', 31 | 'icon' => 'icon', 32 | 'modal' => 'modal', 33 | 'form.checkbox' => 'checkbox', 34 | 'form.input' => 'input', 35 | 'tooltip' => 'tooltip', 36 | 'confirmation-message' => 'confirmation-message', 37 | 'loading' => 'loading' 38 | ]; 39 | } 40 | 41 | public function create($view) 42 | { 43 | $this->view = $view; 44 | 45 | return $this; 46 | } 47 | 48 | public function layout($layout, $section = null, $data = []) 49 | { 50 | $this->layout = $layout; 51 | $this->section = $section; 52 | $this->data = $data; 53 | 54 | return $this; 55 | } 56 | 57 | public function section($section) 58 | { 59 | $this->section = $section; 60 | 61 | return $this; 62 | } 63 | 64 | public function data($data) 65 | { 66 | $this->data = $data; 67 | 68 | return $this; 69 | } 70 | 71 | public function css($options = '') 72 | { 73 | $assets = [ 74 | 'livewire' => Livewire::styles(), 75 | 'tailwindcss' => '', 76 | 'laravel-views' => '' 77 | ]; 78 | 79 | return $this->getCustomizedLinks($assets, $options); 80 | } 81 | 82 | public function js($options = '') 83 | { 84 | $assets = [ 85 | 'livewire' => Livewire::scripts(), 86 | 'alpine' => '', 87 | 'laravel-views' => ' 88 | 89 | ' 90 | ]; 91 | 92 | return $this->getCustomizedLinks($assets, $options); 93 | } 94 | 95 | public function render() 96 | { 97 | $bladeFile = ($this->layout && $this->section) ? 'render-in-layout' : 'render'; 98 | 99 | return view("laravel-views::core.{$bladeFile}", array_merge( 100 | [ 101 | 'view' => $this->view, 102 | 'layout' => $this->layout, 103 | 'section' => $this->section, 104 | ], 105 | $this->data 106 | ))->render(); 107 | } 108 | 109 | private function getCustomizedLinks($assets = [], $options = '') 110 | { 111 | if ($options) { 112 | $options = explode(',', $options); 113 | $options[] = 'laravel-views'; 114 | $links = []; 115 | 116 | foreach ($assets as $asset => $link) { 117 | if (in_array($asset, $options)) { 118 | $links[] = $link; 119 | } 120 | } 121 | } else { 122 | $links = $assets; 123 | } 124 | 125 | $linksStr = implode(PHP_EOL, $links); 126 | return <<getKey()` insted of hardcoded `$model->id` 42 | 43 | ## [2.2.1] - 2021-03-29 44 | ### Fixed 45 | - Added `detail-view`, `grid-view` and `list-view` to the views to be published. 46 | 47 | ## [2.2] - 2021-03-01 48 | ### Added 49 | - New list view 50 | - New detail view with 51 | - Default component 52 | - Default component customization 53 | - Multiple components 54 | - Actions 55 | - Feature to set the width for each column in the TableView 56 | - Documentation for the new detail view 57 | - New `attributes-list` component 58 | - New `layout` component 59 | 60 | ### Changed 61 | - The `title` action now is included as an attribute in the `a` tag for each action. 62 | - Internal refactors 63 | - Base `DataView` class with all the features to manage the data (filters, search, actions, pagination etc) on the table, grid, and list view. 64 | - Refactored actions feature to be reused as a trait 65 | - Refactored actions test to be reused as a trait 66 | 67 | ### Fixed 68 | - Bug with pagination and filters, the pagination was not being reset after a new search 69 | 70 | ## [2.1] - 2021-01-10 71 | ### Added 72 | - Option to customize which assets will be imported 73 | - The current view instance is passed as a param to the action to be executed 74 | 75 | ### Changed 76 | - The filter button was changed by an icon button 77 | - Updated documentation 78 | - Added how to purge `laravel-views` styles 79 | - Added how to render components using `Livewire` 80 | 81 | 82 | ## [2.0] - 2020-09-13 83 | ### Added 84 | - Added compatibility for Laravel Livewire 2.x 85 | 86 | ## [1.1] - 2020-09-03 87 | ### Added 88 | - New `GridView` class. 89 | - Confirmation message on actions. 90 | - Search input with support for relational fields. 91 | - Sort table view columns 92 | - More UI components for table and grid view. 93 | - Link 94 | - Icons 95 | 96 | ## [1.0.6] - 2020-08-22 97 | - Fixed typos 98 | - Fixed filter stub file name 99 | 100 | ## [1.0.5] - 2020-08-09 101 | ### Fixed 102 | - Fixed default behavior clicking on actions adding a prevent directive 103 | 104 | ## [1.0.4] - 2020-07-26 105 | ### Fixed 106 | - Fixed some typos and syntax errors 107 | - Fixed the query builder wrapping the table view search 108 | 109 | ## [1.0.3] - 2020-07-18 110 | ### Fixed 111 | - Fixed a bug using multiple redirect actions on the same table 112 | - Fixed some typos on the documentation 113 | 114 | ### Added 115 | - Added road map to the documentation 116 | - Added contribution guide to the documentation 117 | 118 | ### Changed 119 | - Moved helpers load to composer 120 | 121 | ## [1.0.2] - 2020-07-13 122 | ### Fixed 123 | - Added support for newer livewire versions 124 | - Fixed some typos on the documentation 125 | - Fixed assets URL 126 | 127 | ## [1.0.1] - 2020-06-27 128 | ### Added 129 | - Added the changelog file 130 | - Added a overflow-y utility to the table view layout 131 | - Added missing `renderIf` documentation to the actions by row section 132 | ### Changed 133 | - Changed inputs border color 134 | ### Fixed 135 | - Fixed some broken links on the documentation 136 | - Bumped websocket-extensions from 0.1.3 to 0.1.4 137 | 138 | ## [1.0.0] - 2020-06-14 139 | ### Added 140 | - Created base LaravelViews class 141 | - Created the TableView component 142 | - Search inpurt feature 143 | - Customizable filters 144 | - Actions by row 145 | - Definied components customization -------------------------------------------------------------------------------- /src/Views/DataView.php: -------------------------------------------------------------------------------- 1 | ['except' => ''], 18 | 'filters', 19 | 'sortBy', 20 | 'sortOrder' 21 | ]; 22 | 23 | /** 24 | * (Override) int Number of items to be showed, 25 | * @var int $paginate 26 | */ 27 | protected $paginate = 20; 28 | 29 | /** @var int $total Total of items found */ 30 | public $total = 0; 31 | 32 | /** @var String $search Current query string with the search value */ 33 | public $search; 34 | 35 | /** @var String $filters Current query string with the filters value */ 36 | public $filters = []; 37 | 38 | /** @var Array $filtersViews All filters customized in the child class */ 39 | public $filtersViews; 40 | 41 | /** @var Array $searchBy All fields to search */ 42 | public $searchBy; 43 | 44 | public $sortBy = null; 45 | 46 | public $sortOrder = 'asc'; 47 | 48 | public $selected = []; 49 | public $allSelected = false; 50 | 51 | public function hydrate() 52 | { 53 | $this->filtersViews = $this->filters(); 54 | } 55 | 56 | public function mount(QueryStringData $queryStringData) 57 | { 58 | $this->filtersViews = $this->filters(); 59 | $this->search = $queryStringData->getSearchValue($this->search); 60 | 61 | $this->filters = $queryStringData->getFilterValues($this->filters); 62 | 63 | $this->applyDefaultFilters(); 64 | 65 | $this->sortBy = $queryStringData->getValue('sortBy', $this->sortBy); 66 | $this->sortOrder = $queryStringData->getValue('sortOrder', $this->sortOrder); 67 | } 68 | 69 | /** 70 | * Check if each of the filters has a default value and it's not already set 71 | */ 72 | public function applyDefaultFilters() 73 | { 74 | foreach ($this->filters() as $filter) { 75 | if (empty($this->filters[$filter->id]) && $filter->defaultValue) { 76 | $this->filters[$filter->id] = $filter->defaultValue; 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Collects all data to be passed to the view, this includes the items searched on the database 83 | * through the filters, this data will be passed to livewire render method 84 | */ 85 | protected function getRenderData() 86 | { 87 | return [ 88 | 'items' => $this->paginatedQuery, 89 | 'actionsByRow' => $this->actionsByRow() 90 | ]; 91 | } 92 | 93 | 94 | /** 95 | * Reset pagination 96 | */ 97 | public function updatingSearch() 98 | { 99 | $this->resetPage(); 100 | } 101 | 102 | public function updatingFilters() 103 | { 104 | $this->resetPage(); 105 | } 106 | 107 | protected function filters() 108 | { 109 | return []; 110 | } 111 | 112 | protected function actionsByRow() 113 | { 114 | return []; 115 | } 116 | 117 | public function getActions() 118 | { 119 | return $this->actionsByRow(); 120 | } 121 | 122 | /** 123 | * Clones the initial query (to avoid modifying it) 124 | * and get a model by an Id 125 | */ 126 | public function getModelWhoFiredAction($id) 127 | { 128 | return (clone $this->initialQuery)->find($id); 129 | } 130 | 131 | public function updatedAllSelected($value) 132 | { 133 | $this->selected = $value ? $this->paginatedQuery->pluck('id')->map(function ($id) { 134 | return (string)$id; 135 | })->toArray() : []; 136 | } 137 | 138 | public function getInitialQueryProperty() 139 | { 140 | if (method_exists($this, 'repository')) { 141 | return $this->repository(); 142 | } 143 | return $this->model::query(); 144 | } 145 | 146 | /** 147 | * Returns the items from the database regarding to the filters selected by the user 148 | * applies the search query, the filters used and the total of items found 149 | */ 150 | public function getQueryProperty(Searchable $searchable, Filterable $filterable, Sortable $sortable) 151 | { 152 | $query = clone $this->initialQuery; 153 | $query = $searchable->searchItems($query, $this->searchBy, $this->search); 154 | $query = $filterable->applyFilters($query, $this->filters(), $this->filters); 155 | $query = $sortable->sortItems($query, $this->sortBy, $this->sortOrder); 156 | 157 | return $query; 158 | } 159 | 160 | public function getPaginatedQueryProperty() 161 | { 162 | return $this->query->paginate($this->paginate); 163 | } 164 | 165 | 166 | /** 167 | * LIVEWIERE ACTIONS ARE HERE 168 | * All these actions are executed from the UI and those aren't for internal use 169 | */ 170 | 171 | /** 172 | * Sets the field the table view data will be sort by 173 | * @param string $field Field to sort by 174 | */ 175 | public function sort($field) 176 | { 177 | if ($this->sortBy === $field) { 178 | $this->sortOrder = $this->sortOrder === 'asc' ? 'desc' : 'asc'; 179 | } else { 180 | $this->sortBy = $field; 181 | $this->sortOrder = 'asc'; 182 | } 183 | } 184 | 185 | public function clearFilters() 186 | { 187 | $this->filters = []; 188 | } 189 | 190 | public function clearSearch() 191 | { 192 | $this->search = ''; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/LaravelViewsServiceProvider.php: -------------------------------------------------------------------------------- 1 | TableViewSearchData::class, 35 | Filterable::class => TableViewFilterData::class, 36 | Sortable::class => TableViewSortData::class, 37 | ]; 38 | 39 | /** 40 | * Register services. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | $file = __DIR__ . '/helpers.php'; 47 | if (file_exists($file)) { 48 | require_once($file); 49 | } 50 | 51 | $this->app->bind('laravel-views', function () { 52 | return new LaravelViews(); 53 | }); 54 | $this->app->bind('variants', function () { 55 | return new Variants; 56 | }); 57 | $this->app->bind('ui', UI::class); 58 | $this->app->bind('header', function () { 59 | return new Header(); 60 | }); 61 | } 62 | 63 | /** 64 | * Bootstrap services. 65 | * 66 | * @return void 67 | */ 68 | public function boot() 69 | { 70 | $this->loadViews() 71 | ->loadCommands() 72 | ->publish() 73 | ->bladeDirectives() 74 | ->loadComponents() 75 | ->configFiles() 76 | ->macros(); 77 | } 78 | 79 | private function publish() 80 | { 81 | $this->publishes([ 82 | __DIR__ . '/../public/laravel-views.js' => public_path('vendor/laravel-views.js'), 83 | __DIR__ . '/../public/laravel-views.css' => public_path('vendor/laravel-views.css'), 84 | __DIR__ . '/../public/tailwind.css' => public_path('vendor/tailwind.css'), 85 | ], 'public'); 86 | 87 | $this->publishes([ 88 | __DIR__ . '/config/laravel-views.php' => config_path('laravel-views.php'), 89 | ], 'config'); 90 | 91 | $this->publishes([ 92 | __DIR__ . '/../resources/views/components' => resource_path('views/vendor/laravel-views/components'), 93 | __DIR__ . '/../resources/views/detail-view' => resource_path('views/vendor/laravel-views/detail-view'), 94 | __DIR__ . '/../resources/views/grid-view' => resource_path('views/vendor/laravel-views/grid-view'), 95 | __DIR__ . '/../resources/views/list-view' => resource_path('views/vendor/laravel-views/list-view'), 96 | __DIR__ . '/../resources/views/table-view' => resource_path('views/vendor/laravel-views/table-view'), 97 | ], 'views'); 98 | 99 | return $this; 100 | } 101 | 102 | private function loadViews() 103 | { 104 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'laravel-views'); 105 | 106 | return $this; 107 | } 108 | 109 | private function loadCommands() 110 | { 111 | if ($this->app->runningInConsole()) { 112 | $this->commands([ 113 | FilterMakeCommand::class, 114 | ActionMakeCommand::class, 115 | TableViewMakeCommand::class, 116 | GridViewMakeCommand::class, 117 | ListViewMakeCommand::class, 118 | MakeViewCommand::class 119 | ]); 120 | } 121 | 122 | return $this; 123 | } 124 | 125 | private function bladeDirectives() 126 | { 127 | $laravelViews = new LaravelViews; 128 | 129 | Blade::directive('laravelViewsScripts', function ($options) use ($laravelViews) { 130 | return $laravelViews->js($options); 131 | }); 132 | 133 | Blade::directive('laravelViewsStyles', function ($options) use ($laravelViews) { 134 | return $laravelViews->css($options); 135 | }); 136 | 137 | return $this; 138 | } 139 | 140 | private function loadComponents() 141 | { 142 | $laravelViews = new LaravelViews; 143 | 144 | // Registers anonymous components 145 | foreach ($laravelViews->components() as $path => $component) { 146 | Blade::component('laravel-views::components.' . $path, 'lv-' . $component); 147 | } 148 | Blade::component('laravel-views::view.layout', 'lv-layout'); 149 | 150 | // Registering class components 151 | Blade::component('lv-dynamic-component', DynamicComponent::class); 152 | 153 | // This is only for laravel 8 154 | // Blade::componentNamespace('LaravelViews\\Views\\Components', 'lv'); 155 | 156 | return $this; 157 | } 158 | 159 | private function configFiles() 160 | { 161 | $this->mergeConfigFrom(__DIR__ . '/config/laravel-views.php', 'laravel-views'); 162 | 163 | return $this; 164 | } 165 | 166 | private function macros() 167 | { 168 | $macros = [ 169 | LaravelViewsTestMacros::class, 170 | StrMacros::class 171 | ]; 172 | 173 | foreach ($macros as $macroClass) { 174 | $macro = new $macroClass; 175 | $macro->register(); 176 | } 177 | 178 | return $this; 179 | } 180 | } 181 | --------------------------------------------------------------------------------