├── LICENSE.md ├── README.md ├── composer.json ├── package-lock.json ├── pint.json ├── resources ├── css │ └── plugin.css ├── lang │ ├── ar │ │ └── components.php │ ├── en │ │ └── components.php │ ├── he │ │ └── components.php │ ├── hu │ │ └── components.php │ ├── id │ │ └── components.php │ ├── kk │ │ └── components.php │ ├── nl │ │ └── components.php │ ├── pl │ │ └── components.php │ └── ru │ │ └── components.php └── views │ └── components │ └── table-repeater.blade.php └── src ├── Components ├── Concerns │ ├── CanBeStreamlined.php │ ├── HasBreakPoints.php │ ├── HasEmptyLabel.php │ ├── HasExtraActions.php │ └── HasHeader.php └── TableRepeater.php ├── Header.php └── TableRepeaterServiceProvider.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) awcodes 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Table Repeater Plugin 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/awcodes/filament-table-repeater.svg?style=flat-square)](https://packagist.org/packages/awcodes/filament-table-repeater) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/awcodes/filament-table-repeater.svg?style=flat-square)](https://packagist.org/packages/awcodes/filament-table-repeater) 5 | 6 | table repeater opengraph image 7 | 8 | ## Upgrade Guide for 2.x to 3.x 9 | 10 | 1. Rename you use statements from `Awcodes\FilamentTableRepeater` to `Awcodes\TableRepeater`. 11 | 2. Run `npm run build` to update your theme file. 12 | 3. See [Headers](#headers) for changes to the `headers()` method. 13 | 14 | ## Installation 15 | 16 | You can install the package via composer: 17 | 18 | ```bash 19 | composer require awcodes/filament-table-repeater 20 | ``` 21 | 22 | In an effort to align with Filament's theming methodology you will need to use a custom theme to use this plugin. 23 | 24 | > [!IMPORTANT] 25 | > If you have not set up a custom theme and are using a Panel follow the instructions in the [Filament Docs](https://filamentphp.com/docs/3.x/panels/themes#creating-a-custom-theme) first. The following applies to both the Panels Package and the standalone Forms package. 26 | 27 | 1. Import the plugin's stylesheet in your theme's css file. 28 | 29 | ```css 30 | @import '/awcodes/filament-table-repeater/resources/css/plugin.css'; 31 | ``` 32 | 33 | 2. Add the plugin's views to your `tailwind.config.js` file. 34 | 35 | ```js 36 | content: [ 37 | '/awcodes/filament-table-repeater/resources/**/*.blade.php', 38 | ] 39 | ``` 40 | 41 | ## Usage 42 | 43 | This field has most of the same functionality of the [Filament Forms Repeater](https://filamentphp.com/docs/3.x/forms/fields/repeater) field. The main exception is that this field can not be collapsed. 44 | 45 | ```php 46 | use Awcodes\TableRepeater\Components\TableRepeater; 47 | use Awcodes\TableRepeater\Header; 48 | 49 | TableRepeater::make('users') 50 | ->headers([ 51 | Header::make('name')->width('150px'), 52 | ]) 53 | ->schema([ 54 | ... 55 | ]) 56 | ->columnSpan('full') 57 | ``` 58 | 59 | ### Headers 60 | 61 | To add headers use the `headers()` method. and pass in an array of `Header` components. 62 | 63 | ```php 64 | use Awcodes\TableRepeater\Header; 65 | 66 | TableRepeater::make('users') 67 | ->headers([ 68 | Header::make('name'), 69 | Header::make('email'), 70 | ]) 71 | ``` 72 | 73 | #### Header Alignment 74 | 75 | To align the headers of the table use the `align()` method, passing in one of the Filament Alignment enums. 76 | 77 | ```php 78 | use Filament\Support\Enums\Alignment; 79 | 80 | Header::make('name') 81 | ->align(Alignment::Center) 82 | ``` 83 | 84 | #### Header Width 85 | 86 | To set the width of the headers of the table use the `width()` method. 87 | 88 | ```php 89 | Header::make('name') 90 | ->width('150px') 91 | ``` 92 | 93 | #### Marking Columns as Required 94 | 95 | To mark a column as required use the `markAsRequired()` method. 96 | 97 | ```php 98 | Header::make('name') 99 | ->markAsRequired() 100 | ``` 101 | 102 | #### Hiding the header 103 | 104 | Even if you do not want to show a header, you should still add them to be compliant with accessibility standards. You can hide the header though with the `renderHeader()` method. 105 | 106 | ```php 107 | TableRepeater::make('users') 108 | ->headers(...) 109 | ->renderHeader(false) 110 | ``` 111 | 112 | ### Labels 113 | 114 | By default, form component labels will be set to hidden. To show them use the `showLabels()` method. 115 | 116 | ```php 117 | TableRepeater::make('users') 118 | ->showLabels() 119 | ``` 120 | 121 | ### Empty State Label 122 | 123 | To customize the text shown when the table is empty, use the `emptyLabel()` method. 124 | 125 | ```php 126 | TableRepeater::make('users') 127 | ->emptyLabel('There are no users registered.') 128 | ``` 129 | 130 | Alternatively, you can hide the empty label with `emptyLabel(false)`. 131 | 132 | ### Break Point 133 | 134 | Below a specific break point the table will render as a set of panels to 135 | make working with data easier on mobile devices. The default is 'md', but 136 | can be overridden with the `stackAt()` method. 137 | 138 | ```php 139 | use Filament\Support\Enums\MaxWidth; 140 | 141 | TableRepeater::make('users') 142 | ->stackAt(MaxWidth::Medium) 143 | ``` 144 | 145 | ### Appearance 146 | 147 | If you prefer for the fields to be more inline with the table. You can change the appearance of the table with the `streamlined()` method. 148 | 149 | ```php 150 | TableRepeater::make('users') 151 | ->streamlined() 152 | ``` 153 | 154 | ### Extra Actions 155 | 156 | TableRepeater supports the same `extraItemActions()` as the native Filament repeater. You may also add extra actions below the table with the `extraActions()` method. These will appear next to the 'Add' button or in place of the 'Add' button if it is hidden. 157 | 158 | ```php 159 | TableRepeater::make('users') 160 | ->extraActions([ 161 | Action::make('exportData') 162 | ->icon('heroicon-m-inbox-arrow-down') 163 | ->action(function (TableRepeater $component): void { 164 | Notification::make('export_data') 165 | ->success() 166 | ->title('Data exported.') 167 | ->send(); 168 | }), 169 | ]) 170 | ``` 171 | 172 | ## Changelog 173 | 174 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 175 | 176 | ## Contributing 177 | 178 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 179 | 180 | ## Security Vulnerabilities 181 | 182 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 183 | 184 | ## Credits 185 | 186 | - [Adam Weston](https://github.com/awcodes) 187 | - [All Contributors](../../contributors) 188 | 189 | ## License 190 | 191 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 192 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awcodes/filament-table-repeater", 3 | "description": "A modified version of the Filament Forms Repeater to display it as a table.", 4 | "keywords": [ 5 | "awcodes", 6 | "laravel", 7 | "filament", 8 | "table repeater", 9 | "plugin" 10 | ], 11 | "homepage": "https://github.com/awcodes/filament-table-repeater", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Adam Weston", 16 | "email": "awcodes1@gmail.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.1", 22 | "filament/forms": "^3.2.116", 23 | "spatie/laravel-package-tools": "^1.13.5" 24 | }, 25 | "require-dev": { 26 | "laravel/pint": "^1.0", 27 | "nunomaduro/collision": "^7.0", 28 | "orchestra/testbench": "^8.0", 29 | "pestphp/pest": "^2.0", 30 | "pestphp/pest-plugin-faker": "^2.0", 31 | "pestphp/pest-plugin-laravel": "^2.0", 32 | "pestphp/pest-plugin-livewire": "^2.0", 33 | "phpunit/phpunit": "^10.0", 34 | "spatie/laravel-ray": "^1.26" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Awcodes\\TableRepeater\\": "src" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Awcodes\\TableRepeater\\Tests\\": "tests/src", 44 | "Awcodes\\TableRepeater\\Tests\\Database\\Factories\\": "tests/database/factories" 45 | } 46 | }, 47 | "scripts": { 48 | "pint": "vendor/bin/pint", 49 | "pest": "vendor/bin/pest" 50 | }, 51 | "config": { 52 | "sort-packages": true, 53 | "allow-plugins": { 54 | "composer/package-versions-deprecated": true, 55 | "pestphp/pest-plugin": true, 56 | "phpstan/extension-installer": true 57 | } 58 | }, 59 | "extra": { 60 | "laravel": { 61 | "providers": [ 62 | "Awcodes\\TableRepeater\\TableRepeaterServiceProvider" 63 | ] 64 | } 65 | }, 66 | "minimum-stability": "dev", 67 | "prefer-stable": true 68 | } 69 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "table-repeater", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "blank_line_before_statement": true, 5 | "concat_space": { 6 | "spacing": "one" 7 | }, 8 | "method_argument_space": true, 9 | "single_trait_insert_per_statement": true, 10 | "types_spaces": { 11 | "space": "single" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /resources/css/plugin.css: -------------------------------------------------------------------------------- 1 | .table-repeater-component .choices input { 2 | min-width: 100% !important; 3 | } 4 | 5 | .table-repeater-column.has-hidden-label .fi-fo-field-wrp > label { 6 | @apply sr-only text-sm !mb-2 block; 7 | } 8 | 9 | .table-repeater-column.has-hidden-label .fi-fo-field-wrp:has(.fi-checkbox-input) > label.fi-fo-checkbox-list-option-label { 10 | @apply not-sr-only; 11 | } 12 | 13 | @media (min-width: calc(theme('screens.sm') + 1px)) { 14 | .table-repeater-component.break-point-sm { 15 | &.streamlined { 16 | .fi-input-wrp, 17 | .fi-fo-file-upload .filepond--root { 18 | @apply ring-0 bg-transparent shadow-none; 19 | } 20 | 21 | .fi-fo-field-wrp:has(.fi-fo-checkbox-list), 22 | .fi-fo-field-wrp:has(.fi-checkbox-input), 23 | .fi-fo-field-wrp:has(.fi-fo-radio) { 24 | @apply py-2 px-3; 25 | } 26 | 27 | .fi-fo-field-wrp:has(.fi-fo-toggle) { 28 | @apply inline-block mt-1; 29 | } 30 | } 31 | 32 | .table-repeater-row { 33 | @apply divide-x rtl:divide-x-reverse divide-gray-950/5 dark:divide-white/20; 34 | } 35 | 36 | .table-repeater-row-actions { 37 | @apply justify-center; 38 | } 39 | } 40 | } 41 | 42 | @media (min-width: calc(theme('screens.md') + 1px)) { 43 | .table-repeater-component.break-point-md { 44 | &.streamlined { 45 | .fi-input-wrp, 46 | .fi-fo-file-upload .filepond--root { 47 | @apply ring-0 bg-transparent shadow-none; 48 | } 49 | 50 | .fi-fo-field-wrp:has(.fi-fo-checkbox-list), 51 | .fi-fo-field-wrp:has(.fi-checkbox-input), 52 | .fi-fo-field-wrp:has(.fi-fo-radio) { 53 | @apply py-2 px-3; 54 | } 55 | 56 | .fi-fo-field-wrp:has(.fi-fo-toggle) { 57 | @apply inline-block mt-1; 58 | } 59 | } 60 | 61 | .table-repeater-row { 62 | @apply divide-x rtl:divide-x-reverse divide-gray-950/5 dark:divide-white/20; 63 | } 64 | 65 | .table-repeater-row-actions { 66 | @apply justify-center; 67 | } 68 | } 69 | } 70 | 71 | @media (min-width: calc(theme('screens.lg') + 1px)) { 72 | .table-repeater-component.break-point-lg { 73 | &.streamlined { 74 | .fi-input-wrp, 75 | .fi-fo-file-upload .filepond--root { 76 | @apply ring-0 bg-transparent shadow-none; 77 | } 78 | 79 | .fi-fo-field-wrp:has(.fi-fo-checkbox-list), 80 | .fi-fo-field-wrp:has(.fi-checkbox-input), 81 | .fi-fo-field-wrp:has(.fi-fo-radio) { 82 | @apply py-2 px-3; 83 | } 84 | 85 | .fi-fo-field-wrp:has(.fi-fo-toggle) { 86 | @apply inline-block mt-1; 87 | } 88 | } 89 | 90 | .table-repeater-row { 91 | @apply divide-x rtl:divide-x-reverse divide-gray-950/5 dark:divide-white/20; 92 | } 93 | 94 | .table-repeater-row-actions { 95 | @apply justify-center; 96 | } 97 | } 98 | } 99 | 100 | @media (min-width: calc(theme('screens.xl') + 1px)) { 101 | .table-repeater-component.break-point-xl { 102 | &.streamlined { 103 | .fi-input-wrp, 104 | .fi-fo-file-upload .filepond--root { 105 | @apply ring-0 bg-transparent shadow-none; 106 | } 107 | 108 | .fi-fo-field-wrp:has(.fi-fo-checkbox-list), 109 | .fi-fo-field-wrp:has(.fi-checkbox-input), 110 | .fi-fo-field-wrp:has(.fi-fo-radio) { 111 | @apply py-2 px-3; 112 | } 113 | 114 | .fi-fo-field-wrp:has(.fi-fo-toggle) { 115 | @apply inline-block mt-1; 116 | } 117 | } 118 | 119 | .table-repeater-row { 120 | @apply divide-x rtl:divide-x-reverse divide-gray-950/5 dark:divide-white/20; 121 | } 122 | 123 | .table-repeater-row-actions { 124 | @apply justify-center; 125 | } 126 | } 127 | } 128 | 129 | @media (min-width: calc(theme('screens.2xl') + 1px)) { 130 | .table-repeater-component.break-point-2xl { 131 | &.streamlined { 132 | .fi-input-wrp, 133 | .fi-fo-file-upload .filepond--root { 134 | @apply ring-0 bg-transparent shadow-none; 135 | } 136 | 137 | .fi-fo-field-wrp:has(.fi-fo-checkbox-list), 138 | .fi-fo-field-wrp:has(.fi-checkbox-input), 139 | .fi-fo-field-wrp:has(.fi-fo-radio) { 140 | @apply py-2 px-3; 141 | } 142 | 143 | .fi-fo-field-wrp:has(.fi-fo-toggle) { 144 | @apply inline-block mt-1; 145 | } 146 | } 147 | 148 | .table-repeater-row { 149 | @apply divide-x rtl:divide-x-reverse divide-gray-950/5 dark:divide-white/20; 150 | } 151 | 152 | .table-repeater-row-actions { 153 | @apply justify-center; 154 | } 155 | } 156 | } 157 | 158 | @media (max-width: theme('screens.sm')) { 159 | .table-repeater-component.break-point-sm table { 160 | @apply block w-full; 161 | 162 | thead { @apply sr-only; } 163 | 164 | :is(tbody, th, td) { @apply block !w-full; } 165 | 166 | label { @apply text-start; } 167 | 168 | tr { @apply p-4 !w-full flex flex-col gap-6; } 169 | 170 | tr td:last-of-type ul { @apply px-0; } 171 | 172 | .table-repeater-column.has-hidden-label .fi-fo-field-wrp > label { 173 | @apply not-sr-only; 174 | } 175 | } 176 | } 177 | 178 | @media (max-width: theme('screens.md')) { 179 | .table-repeater-component.break-point-md table { 180 | @apply block w-full; 181 | 182 | thead { @apply sr-only; } 183 | 184 | :is(tbody, th, td) { @apply block !w-full; } 185 | 186 | label { @apply text-start; } 187 | 188 | tr { @apply p-4 !w-full flex flex-col gap-6; } 189 | 190 | tr td:last-of-type ul { @apply px-0; } 191 | 192 | .table-repeater-column.has-hidden-label .fi-fo-field-wrp > label { 193 | @apply not-sr-only; 194 | } 195 | } 196 | } 197 | 198 | @media (max-width: theme('screens.lg')) { 199 | .table-repeater-component.break-point-lg table { 200 | @apply block w-full; 201 | 202 | thead { @apply sr-only; } 203 | 204 | :is(tbody, th, td) { @apply block !w-full; } 205 | 206 | label { @apply text-start; } 207 | 208 | tr { @apply p-4 !w-full flex flex-col gap-6; } 209 | 210 | tr td:last-of-type ul { @apply px-0; } 211 | 212 | .table-repeater-column.has-hidden-label .fi-fo-field-wrp > label { 213 | @apply not-sr-only; 214 | } 215 | } 216 | } 217 | 218 | @media (max-width: theme('screens.xl')) { 219 | .table-repeater-component.break-point-xl table { 220 | @apply block w-full; 221 | 222 | thead { @apply sr-only; } 223 | 224 | :is(tbody, th, td) { @apply block !w-full; } 225 | 226 | label { @apply text-start; } 227 | 228 | tr { @apply p-4 !w-full flex flex-col gap-6; } 229 | 230 | tr td:last-of-type ul { @apply px-0; } 231 | 232 | .table-repeater-column.has-hidden-label .fi-fo-field-wrp > label { 233 | @apply not-sr-only; 234 | } 235 | } 236 | } 237 | 238 | @media (max-width: theme('screens.2xl')) { 239 | .table-repeater-component.break-point-2xl table { 240 | @apply block w-full; 241 | 242 | thead { @apply sr-only; } 243 | 244 | :is(tbody, th, td) { @apply block !w-full; } 245 | 246 | label { @apply text-start; } 247 | 248 | tr { @apply p-4 !w-full flex flex-col gap-6; } 249 | 250 | tr td:last-of-type ul { @apply px-0; } 251 | 252 | .table-repeater-column.has-hidden-label .fi-fo-field-wrp > label { 253 | @apply not-sr-only; 254 | } 255 | } 256 | } 257 | 258 | -------------------------------------------------------------------------------- /resources/lang/ar/components.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'empty' => [ 6 | 'label' => 'لا توجد عناصر', 7 | ], 8 | 'row_actions' => [ 9 | 'label' => 'عمليات الصف', 10 | ], 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /resources/lang/en/components.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'empty' => [ 6 | 'label' => 'No records', 7 | ], 8 | 'row_actions' => [ 9 | 'label' => 'Row Actions', 10 | ], 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /resources/lang/he/components.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'empty' => [ 6 | 'label' => 'אין רשומות', 7 | ], 8 | 'row_actions' => [ 9 | 'label' => 'פעולות שורה', 10 | ], 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /resources/lang/hu/components.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'empty' => [ 6 | 'label' => 'Nincs találat', 7 | ], 8 | 'row_actions' => [ 9 | 'label' => 'Műveletek', 10 | ], 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /resources/lang/id/components.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'empty' => [ 6 | 'label' => 'Tidak ada data', 7 | ], 8 | 'row_actions' => [ 9 | 'label' => 'Tindakan', 10 | ], 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /resources/lang/kk/components.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'empty' => [ 6 | 'label' => 'Жазбалар жоқ', 7 | ], 8 | 'row_actions' => [ 9 | 'label' => 'Жол әрекеттері', 10 | ], 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /resources/lang/nl/components.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'empty' => [ 6 | 'label' => 'Geen gegevens', 7 | ], 8 | 'row_actions' => [ 9 | 'label' => 'Regelacties', 10 | ], 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /resources/lang/pl/components.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'empty' => [ 6 | 'label' => 'Brak rekordów', 7 | ], 8 | 'row_actions' => [ 9 | 'label' => 'Akcje wiersza', 10 | ], 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /resources/lang/ru/components.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'empty' => [ 6 | 'label' => 'Записи отсутствуют', 7 | ], 8 | 'row_actions' => [ 9 | 'label' => 'Действия строки', 10 | ], 11 | ], 12 | ]; 13 | -------------------------------------------------------------------------------- /resources/views/components/table-repeater.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use Filament\Forms\Components\Actions\Action; 3 | use Filament\Support\Enums\Alignment; 4 | use Filament\Support\Enums\MaxWidth; 5 | 6 | $containers = $getChildComponentContainers(); 7 | 8 | $addAction = $getAction($getAddActionName()); 9 | $cloneAction = $getAction($getCloneActionName()); 10 | $deleteAction = $getAction($getDeleteActionName()); 11 | $moveDownAction = $getAction($getMoveDownActionName()); 12 | $moveUpAction = $getAction($getMoveUpActionName()); 13 | $reorderAction = $getAction($getReorderActionName()); 14 | $isReorderableWithButtons = $isReorderableWithButtons(); 15 | $extraItemActions = $getExtraItemActions(); 16 | $extraActions = $getExtraActions(); 17 | $visibleExtraItemActions = []; 18 | $visibleExtraActions = []; 19 | 20 | $headers = $getHeaders(); 21 | $renderHeader = $shouldRenderHeader(); 22 | $stackAt = $getStackAt(); 23 | $hasContainers = count($containers) > 0; 24 | $emptyLabel = $getEmptyLabel(); 25 | $streamlined = $isStreamlined(); 26 | 27 | $statePath = $getStatePath(); 28 | 29 | foreach ($extraActions as $extraAction) { 30 | $visibleExtraActions = array_filter( 31 | $extraActions, 32 | fn (Action $action): bool => $action->isVisible(), 33 | ); 34 | } 35 | 36 | foreach ($extraItemActions as $extraItemAction) { 37 | $visibleExtraItemActions = array_filter( 38 | $extraItemActions, 39 | fn (Action $action): bool => $action->isVisible(), 40 | ); 41 | } 42 | 43 | $hasActions = $reorderAction->isVisible() 44 | || $cloneAction->isVisible() 45 | || $deleteAction->isVisible() 46 | || $moveUpAction->isVisible() 47 | || $moveDownAction->isVisible() 48 | || filled($visibleExtraItemActions); 49 | @endphp 50 | 51 | 52 |
merge($getExtraAttributes())->class([ 55 | 'table-repeater-component space-y-6 relative', 56 | 'streamlined' => $streamlined, 57 | match ($stackAt) { 58 | 'sm', MaxWidth::Small => 'break-point-sm', 59 | 'lg', MaxWidth::Large => 'break-point-lg', 60 | 'xl', MaxWidth::ExtraLarge => 'break-point-xl', 61 | '2xl', MaxWidth::TwoExtraLarge => 'break-point-2xl', 62 | default => 'break-point-md', 63 | } 64 | ]) }} 65 | > 66 | @if (count($containers) || $emptyLabel !== false) 67 |
68 | 69 | ! $renderHeader, 71 | 'table-repeater-header rounded-t-xl overflow-hidden border-b border-gray-950/5 dark:border-white/20' => $renderHeader, 72 | ])> 73 | 74 | @foreach ($headers as $key => $header) 75 | 93 | @endforeach 94 | @if ($hasActions && count($containers)) 95 | 100 | @endif 101 | 102 | 103 | 108 | @if (count($containers)) 109 | @foreach ($containers as $uuid => $row) 110 | @php 111 | $visibleExtraItemActions = array_filter( 112 | $extraItemActions, 113 | fn (Action $action): bool => $action(['item' => $uuid])->isVisible(), 114 | ); 115 | @endphp 116 | 121 | @php($counter = 0) 122 | @foreach($row->getComponents() as $cell) 123 | @if($cell instanceof \Filament\Forms\Components\Hidden || $cell->isHidden()) 124 | {{ $cell }} 125 | @else 126 | 141 | @endif 142 | @endforeach 143 | 144 | @if ($hasActions) 145 | 186 | @endif 187 | 188 | @endforeach 189 | @else 190 | 191 | 195 | 196 | @endif 197 | 198 |
getAlignment()) { 79 | 'center', Alignment::Center => 'text-center', 80 | 'right', 'end', Alignment::Right, Alignment::End => 'text-end', 81 | default => 'text-start' 82 | } 83 | ]) 84 | style="width: {{ $header->getWidth() }}" 85 | > 86 | {{ $header->getLabel() }} 87 | @if ($header->isRequired()) 88 | 89 | * 90 | 91 | @endif 92 | 96 | 97 | {{ trans('table-repeater::components.repeater.row_actions.label') }} 98 | 99 |
! $streamlined, 130 | 'has-hidden-label' => $cell->isLabelHidden(), 131 | match($headers[$counter++]->getAlignment()) { 132 | 'center', Alignment::Center => 'text-center', 133 | 'right', 'end', Alignment::Right, Alignment::End => 'text-end', 134 | default => 'text-start' 135 | } 136 | ]) 137 | style="width: {{ $cell->getMaxWidth() ?? 'auto' }}" 138 | > 139 | {{ $cell }} 140 | 146 |
    147 | @foreach ($visibleExtraItemActions as $extraItemAction) 148 |
  • 149 | {{ $extraItemAction(['item' => $uuid]) }} 150 |
  • 151 | @endforeach 152 | 153 | @if ($reorderAction->isVisible()) 154 |
  • 155 | {{ $reorderAction }} 156 |
  • 157 | @endif 158 | 159 | @if ($isReorderableWithButtons) 160 | @if (! $loop->first) 161 |
  • 162 | {{ $moveUpAction(['item' => $uuid]) }} 163 |
  • 164 | @endif 165 | 166 | @if (! $loop->last) 167 |
  • 168 | {{ $moveDownAction(['item' => $uuid]) }} 169 |
  • 170 | @endif 171 | @endif 172 | 173 | @if ($cloneAction->isVisible()) 174 |
  • 175 | {{ $cloneAction(['item' => $uuid]) }} 176 |
  • 177 | @endif 178 | 179 | @if ($deleteAction->isVisible()) 180 |
  • 181 | {{ $deleteAction(['item' => $uuid]) }} 182 |
  • 183 | @endif 184 |
185 |
193 | {{ $emptyLabel ?: trans('table-repeater::components.repeater.empty.label') }} 194 |
199 |
200 | @endif 201 | 202 | @if ($addAction->isVisible() || filled($visibleExtraActions)) 203 |
    'justify-start', 208 | Alignment::End, Alignment::Right => 'justify-end', 209 | default => 'justify-center', 210 | }, 211 | ]) 212 | > 213 | @if ($addAction->isVisible()) 214 |
  • 215 | {{ $addAction }} 216 |
  • 217 | @endif 218 | @if (filled($visibleExtraActions)) 219 | @foreach ($visibleExtraActions as $extraAction) 220 |
  • 221 | {{ ($extraAction) }} 222 |
  • 223 | @endforeach 224 | @endif 225 |
226 | @endif 227 |
228 |
229 | -------------------------------------------------------------------------------- /src/Components/Concerns/CanBeStreamlined.php: -------------------------------------------------------------------------------- 1 | streamlined = $condition; 14 | 15 | return $this; 16 | } 17 | 18 | public function isStreamlined(): bool 19 | { 20 | return $this->evaluate($this->streamlined) ?? false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Components/Concerns/HasBreakPoints.php: -------------------------------------------------------------------------------- 1 | stackAt = $stackAt; 15 | 16 | return $this; 17 | } 18 | 19 | public function getStackAt(): string | MaxWidth 20 | { 21 | return $this->evaluate($this->stackAt) 22 | ?? MaxWidth::Medium; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Components/Concerns/HasEmptyLabel.php: -------------------------------------------------------------------------------- 1 | emptyLabel = $label; 14 | 15 | return $this; 16 | } 17 | 18 | public function getEmptyLabel(): string | bool | null 19 | { 20 | return $this->evaluate($this->emptyLabel); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Components/Concerns/HasExtraActions.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected array $extraActions = []; 16 | 17 | /** 18 | * @var array | null 19 | */ 20 | protected ?array $cachedExtraActions = null; 21 | 22 | /** 23 | * @param array $actions 24 | */ 25 | public function extraActions(array $actions): static 26 | { 27 | $this->extraActions = [ 28 | ...$this->extraActions, 29 | ...$actions, 30 | ]; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | public function getExtraActions(): array 39 | { 40 | return $this->cachedExtraActions ?? $this->cacheExtraActions(); 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function cacheExtraActions(): array 47 | { 48 | $this->cachedExtraActions = []; 49 | 50 | foreach ($this->extraActions as $extraAction) { 51 | foreach (Arr::wrap($this->evaluate($extraAction)) as $action) { 52 | $this->cachedExtraActions[$action->getName()] = $this->prepareAction( 53 | $action 54 | ->defaultColor('gray') 55 | ->defaultSize(ActionSize::Small) 56 | ->defaultView(Action::BUTTON_VIEW), 57 | ); 58 | } 59 | } 60 | 61 | return $this->cachedExtraActions; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Components/Concerns/HasHeader.php: -------------------------------------------------------------------------------- 1 | headers = $headers; 16 | 17 | return $this; 18 | } 19 | 20 | public function renderHeader(bool | Closure $condition = true): static 21 | { 22 | $this->renderHeader = $condition; 23 | 24 | return $this; 25 | } 26 | 27 | public function getHeaders(): array 28 | { 29 | return $this->evaluate($this->headers) ?? []; 30 | } 31 | 32 | public function shouldRenderHeader(): bool 33 | { 34 | return $this->evaluate($this->renderHeader) ?? true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Components/TableRepeater.php: -------------------------------------------------------------------------------- 1 | registerActions([ 24 | fn (TableRepeater $component): array => $component->getExtraActions() 25 | ]); 26 | } 27 | 28 | public function getChildComponents(): array 29 | { 30 | $components = parent::getChildComponents(); 31 | 32 | if ($this->shouldShowLabels()) { 33 | return $components; 34 | } 35 | 36 | foreach ($components as $component) { 37 | if ( 38 | method_exists($component, 'hiddenLabel') && 39 | ! $component instanceof Placeholder 40 | ) { 41 | $component->hiddenLabel(); 42 | } 43 | } 44 | 45 | return $components; 46 | } 47 | 48 | public function showLabels(bool | Closure | null $condition = true): static 49 | { 50 | $this->showLabels = $condition; 51 | 52 | return $this; 53 | } 54 | 55 | public function shouldShowLabels(): bool 56 | { 57 | return $this->evaluate($this->showLabels) ?? false; 58 | } 59 | 60 | public function getView(): string 61 | { 62 | return 'table-repeater::components.table-repeater'; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Header.php: -------------------------------------------------------------------------------- 1 | $name]); 26 | } 27 | 28 | public function label(string | Htmlable | Closure $label): static 29 | { 30 | $this->label = $label; 31 | 32 | return $this; 33 | } 34 | 35 | public function align(string | Closure | Alignment $align): static 36 | { 37 | $this->align = $align; 38 | 39 | return $this; 40 | } 41 | 42 | public function width(string | Closure $width): static 43 | { 44 | $this->width = $width; 45 | 46 | return $this; 47 | } 48 | 49 | public function markAsRequired(bool | Closure | null $condition = true): static 50 | { 51 | $this->isRequired = $condition; 52 | 53 | return $this; 54 | } 55 | 56 | public function getLabel(): string | Htmlable 57 | { 58 | return $this->evaluate($this->label) 59 | ?? (string) Str::of($this->name)->title(); 60 | } 61 | 62 | public function getAlignment(): string | Alignment 63 | { 64 | return $this->evaluate($this->align) 65 | ?? Alignment::Start; 66 | } 67 | 68 | public function getWidth(): string 69 | { 70 | return $this->evaluate($this->width) 71 | ?? 'auto'; 72 | } 73 | 74 | public function isRequired(): bool 75 | { 76 | return $this->evaluate($this->isRequired) 77 | ?? false; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/TableRepeaterServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('table-repeater') 13 | ->hasAssets() 14 | ->hasTranslations() 15 | ->hasViews(); 16 | } 17 | } 18 | --------------------------------------------------------------------------------