Skip to content

Commit 7dc0898

Browse files
committed
Add datalist functionality to text inputs
1 parent 8fc3110 commit 7dc0898

File tree

2 files changed

+262
-7
lines changed

2 files changed

+262
-7
lines changed

src/Concerns/HasDatalist.php

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
<?php
2+
3+
namespace Backstage\Fields\Concerns;
4+
5+
use Filament\Forms;
6+
use Filament\Forms\Components\Fieldset;
7+
use Filament\Forms\Components\Grid;
8+
use Filament\Forms\Components\Repeater;
9+
use Illuminate\Support\Facades\Schema;
10+
use Illuminate\Support\Str;
11+
12+
trait HasDatalist
13+
{
14+
public static function addDatalistToInput(mixed $input, mixed $field): mixed
15+
{
16+
if (isset($field->config['datalistType']) && $field->config['datalistType'] === 'relationship') {
17+
$options = [];
18+
19+
foreach ($field->config['relations'] as $relation) {
20+
$resources = config('backstage.fields.selectable_resources');
21+
$resourceClass = collect($resources)->first(function ($resource) use ($relation) {
22+
$res = new $resource;
23+
$model = $res->getModel();
24+
$model = new $model;
25+
26+
if (! isset($relation['resource'])) {
27+
return false;
28+
}
29+
30+
return $model->getTable() === $relation['resource'];
31+
});
32+
33+
if (! $resourceClass) {
34+
continue;
35+
}
36+
37+
$resource = new $resourceClass;
38+
$model = $resource->getModel();
39+
$query = $model::query();
40+
41+
// Apply filters if they exist
42+
if (isset($relation['relationValue_filters'])) {
43+
foreach ($relation['relationValue_filters'] as $filter) {
44+
if (isset($filter['column'], $filter['operator'], $filter['value'])) {
45+
$query->where($filter['column'], $filter['operator'], $filter['value']);
46+
}
47+
}
48+
}
49+
50+
$results = $query->get();
51+
52+
if ($results->isEmpty()) {
53+
continue;
54+
}
55+
56+
$opts = $results->pluck($relation['relationValue'] ?? 'name', $relation['relationKey'])->toArray();
57+
58+
if (count($opts) === 0) {
59+
continue;
60+
}
61+
62+
$options[] = $opts;
63+
}
64+
65+
if (! empty($options)) {
66+
$options = array_merge(...$options);
67+
$input->datalist($options);
68+
}
69+
}
70+
71+
if (isset($field->config['datalistType']) && $field->config['datalistType'] === 'array') {
72+
$input->datalist($field->config['options']);
73+
}
74+
75+
return $input;
76+
}
77+
78+
public static function getDatalistConfig(): array
79+
{
80+
return [
81+
'datalistType' => null,
82+
'options' => [],
83+
'descriptions' => [],
84+
'relations' => [],
85+
'contentType' => null,
86+
'relationKey' => null,
87+
'relationValue' => null,
88+
];
89+
}
90+
91+
public function datalistFormFields(): Fieldset
92+
{
93+
return Forms\Components\Fieldset::make('Datalist')
94+
->columnSpanFull()
95+
->label(__('Datalist'))
96+
->schema([
97+
Forms\Components\Grid::make(2)
98+
->schema([
99+
Forms\Components\Select::make('config.datalistType')
100+
->options([
101+
'array' => __('Array'),
102+
'relationship' => __('Relationship'),
103+
])
104+
->searchable()
105+
->live(onBlur: true)
106+
->reactive()
107+
->label(__('Type')),
108+
// Array options
109+
Forms\Components\TagsInput::make('config.options')
110+
->label(__('Options'))
111+
->columnSpanFull()
112+
->placeholder(__('Add option'))
113+
->visible(fn(Forms\Get $get): bool => $get('config.datalistType') == 'array')
114+
->required(fn(Forms\Get $get): bool => $get('config.datalistType') == 'array'),
115+
// Relationship options
116+
Repeater::make('config.relations')
117+
->label(__('Relations'))
118+
->schema([
119+
Grid::make()
120+
->columns(2)
121+
->schema([
122+
Forms\Components\Select::make('resource')
123+
->label(__('Resource'))
124+
->searchable()
125+
->preload()
126+
->columnSpanFull()
127+
->live(debounce: 250)
128+
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
129+
$resources = config('backstage.fields.selectable_resources');
130+
$resourceClass = collect($resources)->first(function ($resource) use ($state) {
131+
$res = new $resource;
132+
$model = $res->getModel();
133+
$model = new $model;
134+
135+
return $model->getTable() === $state;
136+
});
137+
138+
if (! $resourceClass) {
139+
return;
140+
}
141+
142+
$resource = new $resourceClass;
143+
$model = $resource->getModel();
144+
$model = new $model;
145+
146+
// Get all column names from the table
147+
$columns = Schema::getColumnListing($model->getTable());
148+
149+
// Create options array with column names
150+
$columnOptions = collect($columns)->mapWithKeys(function ($column) {
151+
return [$column => Str::title($column)];
152+
})->toArray();
153+
154+
$set('relationValue', null);
155+
$set('relationValue_options', $columnOptions);
156+
})
157+
->options(function () {
158+
$resources = config('backstage.fields.selectable_resources');
159+
160+
return collect($resources)->map(function ($resource) {
161+
$res = new $resource;
162+
$model = $res->getModel();
163+
$model = new $model;
164+
165+
return [
166+
$model->getTable() => Str::title($model->getTable()),
167+
];
168+
})
169+
->collapse()
170+
->toArray();
171+
})
172+
->noSearchResultsMessage(__('No types found'))
173+
->required(fn(Forms\Get $get): bool => $get('config.optionType') == 'relationship'),
174+
Forms\Components\Select::make('relationValue')
175+
->label(__('Column'))
176+
->options(fn(Forms\Get $get) => $get('relationValue_options') ?? [])
177+
->searchable()
178+
->visible(fn(Forms\Get $get): bool => ! empty($get('resource')))
179+
->required(fn(Forms\Get $get): bool => ! empty($get('resource'))),
180+
Forms\Components\Hidden::make('relationKey')
181+
->default('ulid')
182+
->label(__('Key'))
183+
->required(fn(Forms\Get $get): bool => $get('config.optionType') == 'relationship'),
184+
Forms\Components\Repeater::make('relationValue_filters')
185+
->label(__('Filters'))
186+
->visible(fn(Forms\Get $get): bool => ! empty($get('resource')))
187+
->schema([
188+
Forms\Components\Grid::make(3)
189+
->schema([
190+
Forms\Components\Select::make('column')
191+
->options(fn(\Filament\Forms\Get $get) => $get('../../relationValue_options') ?? [
192+
'slug' => __('Slug'),
193+
'name' => __('Name'),
194+
])
195+
->live()
196+
->label(__('Column')),
197+
Forms\Components\Select::make('operator')
198+
->options([
199+
'=' => __('Equal'),
200+
'!=' => __('Not equal'),
201+
'>' => __('Greater than'),
202+
'<' => __('Less than'),
203+
'>=' => __('Greater than or equal to'),
204+
'<=' => __('Less than or equal to'),
205+
'LIKE' => __('Like'),
206+
'NOT LIKE' => __('Not like'),
207+
])
208+
->label(__('Operator')),
209+
Forms\Components\TextInput::make('value')
210+
->datalist(function (Forms\Get $get) {
211+
$resource = $get('../../resource');
212+
$column = $get('column');
213+
214+
if (! $resource || ! $column) {
215+
return [];
216+
}
217+
218+
$resources = config('backstage.fields.selectable_resources');
219+
$resourceClass = collect($resources)->first(function ($r) use ($resource) {
220+
$res = new $r;
221+
$model = $res->getModel();
222+
$model = new $model;
223+
224+
return $model->getTable() === $resource;
225+
});
226+
227+
if (! $resourceClass) {
228+
return [];
229+
}
230+
231+
$resource = new $resourceClass;
232+
$model = $resource->getModel();
233+
234+
return $model::query()
235+
->select($column)
236+
->distinct()
237+
->pluck($column)
238+
->toArray();
239+
})
240+
->label(__('Value')),
241+
]),
242+
])
243+
->columnSpanFull(),
244+
]),
245+
])
246+
->visible(fn(Forms\Get $get): bool => $get('config.datalistType') == 'relationship')
247+
->columnSpanFull(),
248+
]),
249+
]);
250+
}
251+
}

src/Fields/Text.php

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,24 @@
22

33
namespace Backstage\Fields\Fields;
44

5+
use Filament\Forms;
6+
use Backstage\Fields\Models\Field;
57
use Backstage\Fields\Concerns\HasAffixes;
8+
use Backstage\Fields\Concerns\HasDatalist;
69
use Backstage\Fields\Contracts\FieldContract;
7-
use Backstage\Fields\Models\Field;
8-
use Filament\Forms;
910
use Filament\Forms\Components\TextInput as Input;
1011

1112
class Text extends Base implements FieldContract
1213
{
1314
use HasAffixes;
15+
use HasDatalist;
1416

1517
public static function getDefaultConfig(): array
1618
{
1719
return [
1820
...parent::getDefaultConfig(),
1921
...self::getAffixesConfig(),
22+
...self::getDatalistConfig(),
2023
'readOnly' => false,
2124
'autocapitalize' => 'none',
2225
'autocomplete' => null,
@@ -73,7 +76,7 @@ public static function make(string $name, ?Field $field = null): Input
7376
}
7477

7578
$input = self::addAffixesToInput($input, $field);
76-
79+
$input = self::addDatalistToInput($input, $field);
7780
return $input;
7881
}
7982

@@ -107,6 +110,7 @@ public function getForm(): array
107110
->default(false)
108111
->label(__('Autocomplete')),
109112
self::affixFormFields(),
113+
self::datalistFormFields(),
110114
Forms\Components\TextInput::make('config.placeholder')
111115
->label(__('Placeholder')),
112116
Forms\Components\TextInput::make('config.mask')
@@ -136,7 +140,7 @@ public function getForm(): array
136140
->numeric()
137141
->minValue(0)
138142
->label(__('Step'))
139-
->visible(fn (Forms\Get $get): bool => $get('config.type') === 'numeric'),
143+
->visible(fn(Forms\Get $get): bool => $get('config.type') === 'numeric'),
140144
Forms\Components\Select::make('config.inputMode')
141145
->label(__('Input mode'))
142146
->options([
@@ -149,13 +153,13 @@ public function getForm(): array
149153
'email' => __('Email'),
150154
'url' => __('URL'),
151155
])
152-
->visible(fn (Forms\Get $get): bool => $get('config.type') === 'numeric'),
156+
->visible(fn(Forms\Get $get): bool => $get('config.type') === 'numeric'),
153157
Forms\Components\Toggle::make('config.revealable')
154158
->label(__('Revealable'))
155-
->visible(fn (Forms\Get $get): bool => $get('config.type') === 'password'),
159+
->visible(fn(Forms\Get $get): bool => $get('config.type') === 'password'),
156160
Forms\Components\TextInput::make('config.telRegex')
157161
->label(__('Telephone regex'))
158-
->visible(fn (Forms\Get $get): bool => $get('config.type') === 'tel'),
162+
->visible(fn(Forms\Get $get): bool => $get('config.type') === 'tel'),
159163
]),
160164
]),
161165
])->columnSpanFull(),

0 commit comments

Comments
 (0)