├── .gitignore ├── .styleci.yml ├── .travis.yml ├── changelog.md ├── composer.json ├── config └── livewire-tables.php ├── contributing.md ├── license.md ├── phpunit.xml ├── readme.md └── src ├── Commands ├── MakeLivewireTableCommand.php ├── ScaffoldLivewireTableCommand.php ├── component.stub ├── table.stub └── view.stub ├── LivewireModelTable.php └── LivewireTablesServiceProvider.php /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | 8 | before_script: 9 | - travis_retry composer update 10 | 11 | script: 12 | - vendor/bin/phpunit 13 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 4 | - Added `querySql()` method which returns raw SQL generated by query builder 5 | - Refactored much of the query building in `LivewireModelTable` 6 | - Separated the “join” logic out of the sorting method. Related tables are joined / eager loaded via the `joinRelated()` method 7 | - As a result `sortByRelatedField()` is now only responsible for the `orderBy` portion 8 | - Added `generateQueryFields()` which prepares the $fields array for join / search statements. 9 | - Only fields specified in `$fields` are selected with the query 10 | - Removed `whereLike` macro for search, instead loop over supplied searchable fields and build `where` and `orWhere` queries 11 | - [Bug fix]: `whereLike` macro was failing when sort was on related column and search was performed. Now resolved. 12 | 13 | 14 | ## 0.2.2 15 | - Moved CSS scaffolding responsibility to ScaffoldLivewireTableCommand. 16 | - As a result we no longer need to construct parent Livewire component in `LivewireModelTable.php` 17 | 18 | ## 0.2.1 19 | - Fixed pagination error by constructing parent Livewire component 20 | 21 | ## 0.2.0 22 | - Structural and layout changes 23 | 24 | ## 0.1.0 25 | - Initial release / concept 26 | 27 | ### Added 28 | - Everything 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coryrose/livewire-tables", 3 | "description": "An extension for Livewire that allows you to effortlessly scaffold datatables with optional pagination, search, and sort.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Cory Rosenwald", 8 | "email": "coryrosenwald@gmail.com", 9 | "homepage": "https://coryrose.dev" 10 | } 11 | ], 12 | "homepage": "https://github.com/coryrose1/livewire-tables", 13 | "keywords": ["Laravel", "Livewire", "livewire-tables"], 14 | "require": { 15 | "illuminate/support": "~5|~6" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "~7.0", 19 | "mockery/mockery": "^1.1", 20 | "orchestra/testbench": "~3.0", 21 | "sempro/phpunit-pretty-print": "^1.0", 22 | "livewire/livewire": "^0.3.17" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Coryrose\\LivewireTables\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Coryrose\\LivewireTables\\Tests\\": "tests" 32 | } 33 | }, 34 | "extra": { 35 | "laravel": { 36 | "providers": [ 37 | "Coryrose\\LivewireTables\\LivewireTablesServiceProvider" 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/livewire-tables.php: -------------------------------------------------------------------------------- 1 | 'App\\Http\\Livewire\\Tables', 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | View Path 19 | |-------------------------------------------------------------------------- 20 | | 21 | | This value sets the path for Livewire table component views. This effects 22 | | File manipulation helper commands like `artisan livewire-tables:make` 23 | | 24 | */ 25 | 'view_path' => resource_path('views/livewire/tables'), 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Default CSS configuration 29 | |-------------------------------------------------------------------------- 30 | | 31 | | Use these values to set default CSS classes for the corresponding elements. 32 | | 33 | | 34 | */ 35 | 'css' => [ 36 | 'wrapper' => null, 37 | 'table' => null, 38 | 'thead' => null, 39 | 'th' => null, 40 | 'tbody' => null, 41 | 'tr' => null, 42 | 'td' => null, 43 | 'search_wrapper' => null, 44 | 'search_input' => null, 45 | 'sorted_asc' => null, 46 | 'sorted_desc' => null, 47 | 'pagination_wrapper' => null, 48 | ], 49 | ]; 50 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. 4 | 5 | Contributions are accepted via Pull Requests on [Github](https://github.com/coryrose/livewire-tables). 6 | 7 | # Things you could do 8 | If you want to contribute but do not know where to start, this list provides some starting points. 9 | - Add license text 10 | - Help with testing! 11 | - Set up TravisCI, ScrutinizerCI 12 | - Write a comprehensive ReadMe 13 | 14 | ## Pull Requests 15 | 16 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 17 | 18 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. 19 | 20 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 25 | 26 | 27 | **Happy coding**! 28 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # The license 2 | 3 | Copyright (c) 2019 Cory Rosenwald 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Livewire-Tables 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Total Downloads][ico-downloads]][link-downloads] 5 | [![Build Status][ico-travis]][link-travis] 6 | [![StyleCI][ico-styleci]][link-styleci] 7 | 8 | An extension for [Livewire](https://laravel-livewire.com/docs/quickstart/) that allows you to effortlessly scaffold datatables with optional pagination, search, and sort. 9 | 10 | Live demo website will be available soon. 11 | 12 | ## Installation 13 | 14 | Via Composer 15 | 16 | ``` bash 17 | $ composer require coryrose/livewire-tables 18 | ``` 19 | 20 | The package will automatically register its service provider. 21 | 22 | To publish the configuration file to `config/livewire-tables.php` run: 23 | 24 | ``` 25 | php artisan vendor:publish --provider="Coryrose\LivewireTables\LivewireTablesServiceProvider" 26 | ``` 27 | 28 | ## Usage 29 | 30 | Livewire tables are created in three simple steps: 31 | 1. [Create a table component class](#create-a-table-component-class) 32 | 2. [Configure the table class using the available options](#configure-the-component-options) 33 | 3. [Scaffold the table view (as needed when component class changes)](#scaffold-the-table-view) 34 | 35 | ### Create a table component class 36 | Run the make command to generate a table class: 37 | 38 | `php artisan livewire-tables:make UsersTable` 39 | 40 | *App/Http/Livewire/Tables/UsersTable.php* 41 | 42 | ``` 43 | ... 44 | 45 | class UsersTable extends LivewireModelTable 46 | { 47 | use WithPagination; 48 | 49 | public $paginate = true; 50 | public $pagination = 10; 51 | public $hasSearch = true; 52 | 53 | public $fields = [ 54 | [ 55 | 'title' => 'ID', 56 | 'name' => 'id', 57 | 'header_class' => '', 58 | 'cell_class' => '', 59 | 'sortable' => true, 60 | ], 61 | [ 62 | 'title' => 'Name', 63 | 'name' => 'name', 64 | 'header_class' => '', 65 | 'cell_class' => '', 66 | 'sortable' => true, 67 | 'searchable' => true, 68 | ], 69 | [ 70 | 'title' => 'City', 71 | 'name' => 'address.city', 72 | 'header_class' => 'bolded', 73 | 'cell_class' => 'bolded bg-green', 74 | 'sortable' => true, 75 | 'searchable' => true, 76 | ], 77 | [ 78 | 'title' => 'Post', 79 | 'name' => 'post.content', 80 | 'header_class' => '', 81 | 'cell_class' => '', 82 | 'sortable' => true, 83 | 'searchable' => true, 84 | ], 85 | ]; 86 | 87 | public function render() 88 | { 89 | return view('livewire.tables.users-table', [ 90 | 'rowData' => $this->query(), 91 | ]); 92 | } 93 | 94 | public function model() 95 | { 96 | return User::class; 97 | } 98 | 99 | public function with() 100 | { 101 | return ['address', 'post']; 102 | } 103 | } 104 | ``` 105 | 106 | ### Configure the component options 107 | 108 | First, set your base model in the `model()` method in the following format: 109 | ``` 110 | public function model() 111 | { 112 | return User::class; 113 | } 114 | ``` 115 | 116 | To eager load relationships, use the `with()` and return an array of relation(s): 117 | ``` 118 | public function with() 119 | { 120 | return ['address', 'post']; 121 | } 122 | ``` 123 | 124 | The following are editable public properties for the table class: 125 | 126 | | key | description | value | default 127 | | ------------- | ------------- | ------------- | ------------- | 128 | | $paginate | Controls whether the data query & results are paginated. If true, the class must `use WithPagination;` | bool | true 129 | | $pagination | The number value to paginate with | integer | 10 130 | | $hasSearch | Controls global appearance of search bar | bool | true 131 | | [$fields](#$fields) | The fields configuration for your table | array | null 132 | | [$css](#$css) | Per-table CSS settings | array | null 133 | 134 | #### $fields 135 | Controls the field configuration for your table 136 | 137 | | key | description | value 138 | | ------------- | ------------- | ------------- | 139 | | title | Set the displayed column title | string 140 | | name | Should represent the database field name. Use '.' notation for related columns, such as `user.address` | string 141 | | header_class | Set a class for the `` tag for this field | string or null 142 | | cell_class | Set a class for the `` tag for this field | string or null 143 | | sortable | Control whether or not the column is sortable | bool or null 144 | | searchable | Control whether or not the column is searchable | bool or null 145 | 146 | #### $css 147 | Used to generate CSS classes when scaffolding the table. 148 | 149 | These can be set globally in the configuration file, or on a per-table basis in the component class. 150 | 151 | *Note:* CSS classes set in the component will override those from the configuration file where both exist. 152 | 153 | | key | description | value 154 | | ------------- | ------------- | ------------- | 155 | | wrapper | CSS class for `
` surrounding table | string or null 156 | | table | CSS class for `` | string or null 157 | | thead | CSS class for `` | string or null 158 | | th | CSS class for `` | string or null 160 | | tr | CSS class for `` | string or null 161 | | td | CSS class for `'; 73 | if (end($fields) !== $field) { 74 | $cell .= PHP_EOL; 75 | } 76 | array_push($cells, $cell); 77 | } 78 | 79 | return implode('', $cells); 80 | } 81 | 82 | protected function getDataClass(array $field, array $css) 83 | { 84 | if ((isset($css['td']) && $css['td'] && $css['td'] !== '') || (isset($field['cell_class']) && $field['cell_class'] && $field['cell_class'] !== '')) { 85 | if ((isset($css['td']) && $css['td'] && $css['td'] !== '') && (isset($field['cell_class']) && $field['cell_class'] && $field['cell_class'] !== '')) { 86 | return ' class="'.$css['td'].' '.$field['cell_class'].'"'; 87 | } elseif (isset($css['td']) && $css['td'] && $css['td'] !== '') { 88 | return ' class="'.$css['td'].'"'; 89 | } else { 90 | return ' class="'.$field['cell_class'].'"'; 91 | } 92 | } 93 | } 94 | 95 | protected function getHeaderClass(array $field, array $css) 96 | { 97 | if ((isset($css['th']) && $css['th'] && $css['th'] !== '') || (isset($field['header_class']) && $field['header_class'] && $field['header_class'] !== '')) { 98 | if ((isset($css['th']) && $css['th'] && $css['th'] !== '') && (isset($field['header_class']) && $field['header_class'] && $field['header_class'] !== '')) { 99 | return ' class="'.$css['th'].' '.$field['header_class'].'"'; 100 | } elseif (isset($css['th']) && $css['th'] && $css['th'] !== '') { 101 | return ' class="'.$css['th'].'"'; 102 | } else { 103 | return ' class="'.$field['header_class'].'"'; 104 | } 105 | } 106 | } 107 | 108 | protected function getSortableAction($key, array $field) 109 | { 110 | if (isset($field['sortable']) && $field['sortable']) { 111 | return ' wire:click="$emit(\'sortColumn\', '.$key.')"'; 112 | } 113 | } 114 | 115 | /** 116 | * @param $css 117 | * @return array 118 | */ 119 | protected function constructCssPatternsAndReplacements($css): array 120 | { 121 | $patterns = collect(array_keys($css)); 122 | $patterns->each(function ($pattern, $key) use ($patterns) { 123 | $patterns[$key] = '['.$pattern.']'; 124 | }); 125 | $replacements = collect(array_values($css)); 126 | $replacements->each(function ($replacement, $key) use ($replacements) { 127 | ! is_null($replacement) ? $replacements[$key] = ' class="'.$replacement.'"' : $replacements[$key] = ''; 128 | }); 129 | 130 | return [$patterns, $replacements]; 131 | } 132 | 133 | /** 134 | * @param \ReflectionClass $tableComponent 135 | * @return array 136 | */ 137 | protected function constructFieldsAndCss(ReflectionClass $tableComponent): array 138 | { 139 | $properties = $tableComponent->getDefaultProperties(); 140 | $fields = $properties['fields']; 141 | $css = $this->setCssArray($properties['css']); 142 | 143 | return [$fields, $css]; 144 | } 145 | 146 | protected function setCssArray($css): array 147 | { 148 | if (isset($css) && $css !== null) { 149 | return array_merge(config('livewire-tables.css'), $css); 150 | } else { 151 | return config('livewire-tables.css', []); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Commands/component.stub: -------------------------------------------------------------------------------- 1 | 'ID', 20 | 'name' => 'id', 21 | 'header_class' => '', 22 | 'cell_class' => '', 23 | 'sortable' => true, 24 | 'searchable' => true, 25 | ] 26 | ]; 27 | 28 | public function render() 29 | { 30 | return view('[view]', [ 31 | 'rowData' => $this->query(), 32 | ]); 33 | } 34 | 35 | public function model() 36 | { 37 | return User::class; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Commands/table.stub: -------------------------------------------------------------------------------- 1 | 2 | @if ($hasSearch) 3 | 4 |
5 | 6 | @if ($search) 7 | 8 | @endif 9 |
10 | 11 | @endif 12 | 13 | 14 | 15 | [header] 16 |
17 | 18 | 19 | @foreach ($rowData as $row) 20 | 21 | [data] 22 | 23 | @endforeach 24 | 25 |
` | string or null 159 | | tbody | CSS class for `
` | string or null 162 | | search_wrapper | CSS class for `
` surrounding search | string or null 163 | | search_input | CSS class for search `` | string or null 164 | | pagination_wrapper | CSS class for `
` surrounding pagination buttons | string or null 165 | 166 | 167 | ### Scaffold the table view 168 | 169 | When ready, scaffold the table view using the scaffold command: 170 | 171 | `php artisan livewire-tables:scaffold UsersTable` 172 | 173 | *resources/views/livewire/tables/users-table.blade.php* 174 | 175 | ``` 176 |
177 | @if ($hasSearch) 178 |
179 |
180 | 181 | @if ($search) 182 | 183 | @endif 184 |
185 |
186 | @endif 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | @foreach ($rowData as $row) 198 | 199 | 200 | 201 | 202 | 203 | 204 | @endforeach 205 | 206 |
IDNameCityPost
{{ $row->id }}{{ $row->name }}{{ $row->address->city }}{{ $row->post->content }}
207 | @if ($paginate) 208 |
209 | {{ $rowData->links() }} 210 |
211 | @endif 212 |
213 | ``` 214 | 215 | You can use the scaffold command continuously as you make changes to the parent component class. 216 | 217 | Since the rendered template is simple HTML, there’s no need for table “slots” for customization - customize the template as you see fit! 218 | 219 | ## Todo 220 | - Further support for more advanced queries than a model 221 | 222 | ## Change log 223 | 224 | Please see the [changelog](changelog.md) for more information on what has changed recently. 225 | 226 | ## Contributing 227 | 228 | Please see [contributing.md](contributing.md) for details and a todolist. 229 | 230 | ## Credits 231 | 232 | - [Cory Rosenwald][link-author] 233 | - [Laravel Livewire](https://laravel-livewire.com/docs/quickstart/) 234 | - [All Contributors][link-contributors] 235 | 236 | ## License 237 | 238 | MIT. Please see the [license file](license.md) for more information. 239 | 240 | [ico-version]: https://img.shields.io/packagist/v/coryrose/livewire-tables.svg?style=flat-square 241 | [ico-downloads]: https://img.shields.io/packagist/dt/coryrose/livewire-tables.svg?style=flat-square 242 | [ico-travis]: https://img.shields.io/travis/coryrose/livewire-tables/master.svg?style=flat-square 243 | [ico-styleci]: https://styleci.io/repos/12345678/shield 244 | 245 | [link-packagist]: https://packagist.org/packages/coryrose/livewire-tables 246 | [link-downloads]: https://packagist.org/packages/coryrose/livewire-tables 247 | [link-travis]: https://travis-ci.org/coryrose/livewire-tables 248 | [link-styleci]: https://styleci.io/repos/12345678 249 | [link-author]: https://github.com/coryrose 250 | [link-contributors]: ../../contributors 251 | -------------------------------------------------------------------------------- /src/Commands/MakeLivewireTableCommand.php: -------------------------------------------------------------------------------- 1 | parser = new ComponentParser( 21 | config('livewire.class_namespace', 'App\\Http\\Livewire\\Tables'), 22 | config('livewire.view_path', resource_path('views/livewire/tables')), 23 | $this->argument('name') 24 | ); 25 | 26 | $force = $this->option('force'); 27 | 28 | $class = $this->createClass($force); 29 | $view = $this->createView($force); 30 | 31 | $this->refreshComponentAutodiscovery(); 32 | 33 | ($class && $view) && $this->line(" TABLE COMPONENT CREATED 🤙\n"); 34 | $class && $this->line("CLASS: {$this->parser->relativeClassPath()}"); 35 | $view && $this->line("VIEW: {$this->parser->relativeViewPath()}"); 36 | } 37 | 38 | protected function createClass($force = false) 39 | { 40 | $classPath = $this->parser->classPath(); 41 | if (File::exists($classPath) && ! $force) { 42 | $this->line(" WHOOPS-IE-TOOTLES 😳 \n"); 43 | $this->line("Class already exists: {$this->parser->relativeClassPath()}"); 44 | 45 | return false; 46 | } 47 | $this->ensureDirectoryExists($classPath); 48 | File::put($classPath, $this->classContents()); 49 | 50 | return $classPath; 51 | } 52 | 53 | protected function createView($force = false) 54 | { 55 | $viewPath = $this->parser->viewPath(); 56 | if (File::exists($viewPath) && ! $force) { 57 | $this->line("View already exists: {$this->parser->relativeViewPath()}"); 58 | 59 | return false; 60 | } 61 | $this->ensureDirectoryExists($viewPath); 62 | File::put($viewPath, $this->viewContents()); 63 | 64 | return $viewPath; 65 | } 66 | 67 | public function classContents() 68 | { 69 | $template = file_get_contents(__DIR__.DIRECTORY_SEPARATOR.'component.stub'); 70 | 71 | return preg_replace_array( 72 | ['/\[namespace\]/', '/\[class\]/', '/\[view\]/'], 73 | [$this->parser->classNamespace(), $this->parser->className(), $this->viewName()], 74 | $template 75 | ); 76 | } 77 | 78 | public function viewContents() 79 | { 80 | $template = file_get_contents(__DIR__.DIRECTORY_SEPARATOR.'view.stub'); 81 | 82 | return preg_replace( 83 | '/\[class\]/', 84 | $this->parser->className(), 85 | $template 86 | ); 87 | } 88 | 89 | public function viewName() 90 | { 91 | $directories = preg_split('/[.]+/', $this->argument('name')); 92 | 93 | $component = Str::kebab(array_pop($directories)); 94 | $directories = array_map([Str::class, 'studly'], $directories); 95 | 96 | return collect() 97 | ->push('livewire.tables') 98 | ->concat($directories) 99 | ->map([Str::class, 'kebab']) 100 | ->push($component) 101 | ->implode('.'); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Commands/ScaffoldLivewireTableCommand.php: -------------------------------------------------------------------------------- 1 | parser = new ComponentParser(config('livewire.class_namespace', 'App\\Http\\Livewire\\Tables'), 22 | config('livewire.view_path', resource_path('views/livewire/tables')), $this->argument('name')); 23 | $view = $this->createView(); 24 | $view && $this->line(' Table Scaffolded '); 25 | $view && $this->line("VIEW: {$this->parser->relativeViewPath()}"); 26 | } 27 | 28 | protected function createView() 29 | { 30 | $viewPath = $this->parser->viewPath(); 31 | $this->ensureDirectoryExists($viewPath); 32 | File::put($viewPath, $this->viewContents()); 33 | 34 | return $viewPath; 35 | } 36 | 37 | protected function viewContents() 38 | { 39 | $template = file_get_contents(__DIR__.DIRECTORY_SEPARATOR.'table.stub'); 40 | 41 | // We need access to the table component class to retrieve and construct the fields and css 42 | $tableComponent = new ReflectionClass($this->parser->baseClassNamespace.'\\'.$this->parser->className()); 43 | [$fields, $css] = $this->constructFieldsAndCss($tableComponent); 44 | [$patterns, $replacements] = $this->constructCssPatternsAndReplacements($css); 45 | 46 | // Replace the contents of the sub with header and data rows and css classes 47 | return preg_replace('/\[header\]/', $this->headerRows($fields, $css), 48 | preg_replace('/\[data\]/', $this->dataRows($fields, $css), 49 | str_replace($patterns->toArray(), $replacements->toArray(), $template))); 50 | } 51 | 52 | protected function headerRows(array $fields, array $css) 53 | { 54 | $cells = []; 55 | foreach ($fields as $key => $field) { 56 | $cell = "\t\t\tgetHeaderClass($field, $css).$this->getSortableAction($key, 57 | $field).'>'.$field['title'].''; 58 | if (end($fields) !== $field) { 59 | $cell .= PHP_EOL; 60 | } 61 | array_push($cells, $cell); 62 | } 63 | 64 | return implode('', $cells); 65 | } 66 | 67 | protected function dataRows(array $fields, array $css) 68 | { 69 | $cells = []; 70 | foreach ($fields as $key => $field) { 71 | $cell = "\t\t\t\tgetDataClass($field, $css).'>{{ $row->'.str_replace('.', '->', 72 | $field['name']).' }}
26 | @if ($paginate) 27 | 28 | {{ $rowData->links() }} 29 |
30 | @endif 31 | 32 | -------------------------------------------------------------------------------- /src/Commands/view.stub: -------------------------------------------------------------------------------- 1 |
2 | Build out [class].php and run php artisan livewire-tables:scaffold [class] to generate this table. 3 |
4 | -------------------------------------------------------------------------------- /src/LivewireModelTable.php: -------------------------------------------------------------------------------- 1 | 'setSort']; 20 | 21 | public function setSort($column) 22 | { 23 | $this->sortField = array_key_exists('sort_field', 24 | $this->fields[$column]) ? $this->fields[$column]['sort_field'] : $this->fields[$column]['name']; 25 | if (! $this->sortDir) { 26 | $this->sortDir = 'asc'; 27 | } elseif ($this->sortDir == 'asc') { 28 | $this->sortDir = 'desc'; 29 | } else { 30 | $this->sortDir = null; 31 | $this->sortField = null; 32 | } 33 | } 34 | 35 | protected function query() 36 | { 37 | return $this->paginate($this->buildQuery()); 38 | } 39 | 40 | protected function querySql() 41 | { 42 | return $this->buildQuery()->toSql(); 43 | } 44 | 45 | protected function buildQuery() 46 | { 47 | $model = app($this->model()); 48 | $query = $model->newQuery(); 49 | $queryFields = $this->generateQueryFields($model); 50 | if ($this->with()) { 51 | $query = $this->joinRelated($query, $model); 52 | if ($this->sortIsRelatedField()) { 53 | $query = $this->sortByRelatedField($query, $model); 54 | } else { 55 | $query = $this->sort($query); 56 | } 57 | } else { 58 | $query = $this->sort($query); 59 | } 60 | $query = $this->setSelectFields($query, $queryFields); 61 | if ($this->hasSearch && $this->search && $this->search !== '') { 62 | $query = $this->search($query, $queryFields); 63 | } 64 | 65 | return $query; 66 | } 67 | 68 | protected function sort($query) 69 | { 70 | if (! $this->sortField || ! $this->sortDir) { 71 | return $query; 72 | } 73 | 74 | return $query->orderBy($this->sortField, $this->sortDir); 75 | } 76 | 77 | protected function search($query, $queryFields) 78 | { 79 | $searchFields = $queryFields->where('searchable', true)->pluck('name'); 80 | $firstSearch = $searchFields->shift(); 81 | $query = $query->where($firstSearch, 'LIKE', "%{$this->search}%"); 82 | if ($searchFields->count() > 0) { 83 | foreach ($searchFields->toArray() as $searchField) { 84 | $query = $query->orWhere($searchField, 'LIKE', "%{$this->search}%"); 85 | } 86 | } 87 | 88 | return $query; 89 | } 90 | 91 | protected function paginate($query) 92 | { 93 | if (! $this->paginate) { 94 | return $query->get(); 95 | } 96 | 97 | return $query->paginate($this->pagination ?? 15); 98 | } 99 | 100 | protected function sortByRelatedField($query, $model) 101 | { 102 | $relations = collect(explode('.', $this->sortField)); 103 | $relationship = $relations->first(); 104 | $sortField = $relations->pop(); 105 | 106 | return $query->orderBy($model->{$relationship}()->getRelated()->getTable().'.'.$sortField, $this->sortDir); 107 | } 108 | 109 | public function model() 110 | { 111 | } 112 | 113 | protected function with() 114 | { 115 | return []; 116 | } 117 | 118 | public function clearSearch() 119 | { 120 | $this->search = null; 121 | } 122 | 123 | /** 124 | * @return bool 125 | */ 126 | protected function sortIsRelatedField(): bool 127 | { 128 | return $this->sortField && Str::contains($this->sortField, '.') && $this->sortDir; 129 | } 130 | 131 | protected function joinRelated($query, $model) 132 | { 133 | $query = $query->with($this->with()); 134 | foreach ($this->with() as $relationship) { 135 | $query = $query->leftJoin($model->{$relationship}()->getRelated()->getTable(), 136 | $model->getTable().'.'.$model->getKeyName(), '=', 137 | $model->{$relationship}()->getRelated()->getTable().'.'.$model->{$relationship}()->getForeignKeyName()); 138 | } 139 | 140 | return $query; 141 | } 142 | 143 | protected function setSelectFields($query, $queryFields) 144 | { 145 | return $query->select($queryFields->pluck('name')->toArray()); 146 | } 147 | 148 | protected function generateQueryFields($model) 149 | { 150 | return (collect($this->fields))->transform(function ($selectField) use ($model) { 151 | if ($selectField['name'] == 'id') { 152 | $selectField['name'] = $model->getTable().'.id'; 153 | } elseif (Str::contains($selectField['name'], '.')) { 154 | $fieldParts = explode('.', $selectField['name']); 155 | $selectField['name'] = $model->{$fieldParts[0]}()->getRelated()->getTable().'.'.$fieldParts[1]; 156 | } 157 | 158 | return $selectField; 159 | }); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/LivewireTablesServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadTranslationsFrom(__DIR__.'/../resources/lang', 'coryrose'); 19 | // $this->loadViewsFrom(__DIR__.'/../resources/views', 'coryrose'); 20 | // $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 21 | // $this->loadRoutesFrom(__DIR__.'/routes.php'); 22 | 23 | // Publishing is only necessary when using the CLI. 24 | if ($this->app->runningInConsole()) { 25 | $this->bootForConsole(); 26 | 27 | $this->commands([ 28 | MakeLivewireTableCommand::class, 29 | ScaffoldLivewireTableCommand::class, 30 | ]); 31 | } 32 | } 33 | 34 | /** 35 | * Register any package services. 36 | * 37 | * @return void 38 | */ 39 | public function register() 40 | { 41 | $this->mergeConfigFrom(__DIR__.'/../config/livewire-tables.php', 'livewire-tables'); 42 | } 43 | 44 | /** 45 | * Get the services provided by the provider. 46 | * 47 | * @return array 48 | */ 49 | public function provides() 50 | { 51 | return ['livewire-tables']; 52 | } 53 | 54 | /** 55 | * Console-specific booting. 56 | * 57 | * @return void 58 | */ 59 | protected function bootForConsole() 60 | { 61 | // Publishing the configuration file. 62 | $this->publishes([ 63 | __DIR__.'/../config/livewire-tables.php' => config_path('livewire-tables.php'), 64 | ], 'livewire-tables'); 65 | 66 | // Publishing the views. 67 | /*$this->publishes([ 68 | __DIR__.'/../resources/views' => base_path('resources/views/vendor/coryrose'), 69 | ], 'livewire-tables.views');*/ 70 | 71 | // Publishing assets. 72 | /*$this->publishes([ 73 | __DIR__.'/../resources/assets' => public_path('vendor/coryrose'), 74 | ], 'livewire-tables.views');*/ 75 | 76 | // Publishing the translation files. 77 | /*$this->publishes([ 78 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/coryrose'), 79 | ], 'livewire-tables.views');*/ 80 | 81 | // Registering package commands. 82 | // $this->commands([]); 83 | } 84 | } 85 | --------------------------------------------------------------------------------