Skip to content

Commit 909fbe5

Browse files
author
aguingand
authored
Merge pull request #556 from code16/poc-autocomplete-closure
[9.0] Autocomplete closure
2 parents 7236c75 + ba7bcc5 commit 909fbe5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2064
-1122
lines changed

demo/app/Sharp/Posts/PostForm.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Models\Category;
66
use App\Models\Post;
7+
use App\Models\User;
78
use App\Sharp\Utils\Embeds\AuthorEmbed;
89
use App\Sharp\Utils\Embeds\CodeEmbed;
910
use App\Sharp\Utils\Embeds\RelatedPostEmbed;
@@ -137,9 +138,20 @@ public function buildFormFields(FieldsContainer $formFields): void
137138
SharpFormAutocompleteRemoteField::make('author_id')
138139
->setReadOnly(! auth()->user()->isAdmin())
139140
->setLabel('Author')
140-
->setRemoteEndpoint('/api/admin/users')
141-
->setListItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
142-
->setResultItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
141+
->allowEmptySearch()
142+
->setRemoteCallback(function ($search) {
143+
$users = User::orderBy('name')->limit(10);
144+
145+
foreach (explode(' ', trim($search)) as $word) {
146+
$users->where(fn ($query) => $query
147+
->where('name', 'like', "%$word%")
148+
->orWhere('email', 'like', "%$word%")
149+
);
150+
}
151+
152+
return $users->get();
153+
})
154+
->setListItemTemplate('<div>{{ $name }}</div><div><small>{{ $email }}</small></div>')
143155
->setHelpMessage('This field is only editable by admins.'),
144156
));
145157
}

demo/app/Sharp/TestForm/TestForm.php

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,19 @@ public function buildFormFields(FieldsContainer $formFields): void
3737
)
3838
->addField(
3939
SharpFormAutocompleteLocalField::make('autocomplete_local')
40-
->setLocalized()
4140
->setLabel('Autocomplete local')
4241
->setLocalSearchKeys(['label'])
43-
->setListItemInlineTemplate('{{label}}')
44-
->setResultItemInlineTemplate('{{label}} ({{id}})')
45-
->setLocalValues($this->options(true)),
42+
->setListItemTemplate('{{ $label }}')
43+
->setResultItemTemplate('{{ $label }} ({{ $id }})')
44+
->setLocalValues($this->options()),
4645
)
4746
->addField(
4847
SharpFormAutocompleteRemoteField::make('autocomplete_remote')
4948
->setLabel('Autocomplete remote')
5049
->setRemoteSearchAttribute('query')
51-
->setListItemInlineTemplate('{{name}}')
52-
->setResultItemInlineTemplate('{{name}} ({{num}})')
53-
->setRemoteEndpoint(url('/passengers')),
50+
->setListItemTemplate('{{ $name }}')
51+
->setResultItemTemplate('{{ $name }} ({{ $num }})')
52+
->setRemoteEndpoint('/api/admin/users'),
5453
)
5554
->addField(
5655
SharpFormAutocompleteListField::make('autocomplete_list')
@@ -61,8 +60,8 @@ public function buildFormFields(FieldsContainer $formFields): void
6160
SharpFormAutocompleteRemoteField::make('item')
6261
->setLabel('Passenger')
6362
->setPlaceholder('test')
64-
->setListItemInlineTemplate('{{ name }}')
65-
->setResultItemInlineTemplate('{{name}} ({{num}})')
63+
->setListItemTemplate('{{ $name }}')
64+
->setResultItemTemplate('{{ $name }} ({{ $num }})')
6665
->setRemoteEndpoint(url('/passengers')),
6766
),
6867
)
@@ -209,29 +208,25 @@ public function buildFormFields(FieldsContainer $formFields): void
209208
->setStep(.1),
210209
)
211210
->addField(
212-
SharpFormSelectField::make('select_dropdown', $this->options(true))
213-
->setLocalized()
211+
SharpFormSelectField::make('select_dropdown', $this->options())
214212
->setLabel('Select dropdown')
215213
->setMultiple()
216214
->setDisplayAsDropdown(),
217215
)
218216
->addField(
219-
SharpFormSelectField::make('select_list', $this->options(true))
220-
->setLocalized()
217+
SharpFormSelectField::make('select_list', $this->options())
221218
->setLabel('Select list')
222219
->setDisplayAsList(),
223220
)
224221
->addField(
225-
SharpFormSelectField::make('select_list_multiple', $this->options(true))
226-
->setLocalized()
222+
SharpFormSelectField::make('select_list_multiple', $this->options())
227223
->setLabel('Select list multiple')
228224
->setMultiple()
229225
->setDisplayAsList()
230226
->setMaxSelected(2),
231227
)
232228
->addField(
233-
SharpFormTagsField::make('tags', $this->options(true))
234-
->setLocalized()
229+
SharpFormTagsField::make('tags', $this->options())
235230
->setLabel('Tags')
236231
->setCreatable(true)
237232
->setCreateAttribute('label')
@@ -375,33 +370,19 @@ public function getDataLocalizations(): array
375370
return ['fr', 'en'];
376371
}
377372

378-
protected function options(bool $localized = false): array
373+
protected function options(): array
379374
{
380-
if (! $localized) {
381-
return [
382-
'1' => 'Option one',
383-
'2' => 'Option two',
384-
'3' => 'Option three',
385-
];
386-
}
387-
388375
return [
389-
'1' => ['en' => 'Option one', 'fr' => 'Option un'],
390-
'2' => ['en' => 'Option two', 'fr' => 'Option deux'],
391-
'3' => ['en' => 'Option three', 'fr' => 'Option trois'],
392-
'4' => ['en' => 'Option four', 'fr' => 'Option quatre'],
393-
'5' => ['en' => 'Option five', 'fr' => 'Option cinq'],
394-
'6' => ['en' => 'Option six', 'fr' => 'Option six'],
395-
'7' => ['en' => 'Option seven', 'fr' => 'Option sept'],
396-
'8' => ['en' => 'Option eight', 'fr' => 'Option huit'],
397-
'9' => ['en' => 'Option nine', 'fr' => 'Option neuf'],
398-
'10' => ['en' => 'Option ten', 'fr' => 'Option dix'],
399-
'11' => ['en' => 'Option eleven', 'fr' => 'Option onze'],
400-
'12' => ['en' => 'Option twelve', 'fr' => 'Option douze'],
401-
'13' => ['en' => 'Option thirteen', 'fr' => 'Option treize'],
402-
'14' => ['en' => 'Option fourteen', 'fr' => 'Option quatorze'],
403-
'15' => ['en' => 'Option fifteen', 'fr' => 'Option quinze'],
404-
'16' => ['en' => 'Option sixteen', 'fr' => 'Option seize'],
376+
'1' => 'Option one',
377+
'2' => 'Option two',
378+
'3' => 'Option three',
379+
'4' => 'Option four',
380+
'5' => 'Option five',
381+
'6' => 'Option six',
382+
'7' => 'Option seven',
383+
'8' => 'Option eight',
384+
'9' => 'Option nine',
385+
'10' => 'Option ten',
405386
];
406387
}
407388
}

demo/app/Sharp/Utils/Embeds/AuthorEmbed.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ public function buildFormFields(FieldsContainer $formFields): void
3636
SharpFormAutocompleteRemoteField::make('author')
3737
->setLabel('Author')
3838
->setRemoteEndpoint('/api/admin/users')
39-
->setListItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
40-
->setResultItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>'),
39+
->setListItemTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
40+
// ->setListItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>')
41+
// ->setResultItemInlineTemplate('<div>{{name}}</div><div><small>{{email}}</small></div>'),
4142
)
4243
->addField(
4344
SharpFormUploadField::make('picture')

demo/routes/api.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Illuminate\Http\Request;
66
use Illuminate\Support\Facades\Route;
77

8-
Route::middleware('auth:sanctum')->get('/admin/users', function (Request $request) {
8+
Route::get('/admin/users', function (Request $request) {
99
$users = User::orderBy('name');
1010

1111
foreach (explode(' ', trim($request->query('query'))) as $word) {

docs/guide/form-fields/autocomplete-list.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ You can in fact define the list as this:
1212
SharpFormAutocompleteListField::make('winners')
1313
->setLabel('Winners')
1414
->setItemField(
15-
SharpFormAutocompleteField::make('item', 'remote')
15+
SharpFormAutocompleteRemoteField::make('item')
1616
->setRemoteEndpoint('/players')
1717
// [...]
1818
)
@@ -30,7 +30,7 @@ But why can't we use a classic List for this? Well, the `model->winners` relatio
3030

3131
Configuration is the same as the classic [List](list.md), except for:
3232

33-
### `setItemField(SharpFormAutocompleteField $field)`
33+
### `setItemField(SharpFormAutocompleteRemoteField $field)`
3434

3535
You can use this function instead of `addItemField`, since items of AutocompleteList have only one field.
3636

docs/guide/form-fields/autocomplete.md

Lines changed: 58 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,55 @@ This method is useful to link the dataset of a local autocomplete (aka: the `loc
2121

2222
### `setRemoteEndpoint(string $remoteEndpoint)`
2323

24-
The endpoint to hit with mode=remote.
24+
The remote endpoint which should return JSON-formatted results. Note that you can add the `sharp_auth` middleware to this route to handle authentication and prevent this API endpoint to be called by non-sharp users:
2525

26-
If this endpoint is yours (`remote` mode here is useful to avoid loading a lot of data in the view), you can add the `sharp_auth` middleware to the API route to handle authentication and prevent this API endpoint to be called by non-sharp users:
26+
```php
27+
// in a route file
28+
29+
Route::get('/api/sharp/clients', [MySharpApiClientController::class, 'index'])
30+
->middleware('sharp_auth');
31+
```
32+
33+
::: tip
34+
This endpoint MUST be part of your application. If you need to hit an external endpoint, you should create a custom endpoint in your application that will call the external endpoint (be sure to check the alternative `setRemoteCallback` method).
35+
:::
36+
37+
### `setRemoteCallback(Closure $closure, ?array $linkedFields = null)`
38+
39+
To avoid the pain of writing a new dedicated endpoint, and for simple cases, you can use this method to provide a callback that will be called when the autocomplete field needs to fetch data. The callback will receive the search string as a parameter and should return an array of objects.
40+
41+
Example:
42+
43+
```php
44+
SharpFormAutocompleteRemoteField::make('customer')
45+
->setRemoteCallback(function ($search) {
46+
return Customer::select('id', 'name', 'email')
47+
->where('name', 'like', "%$search%")
48+
->get();
49+
});
50+
```
51+
52+
The second argument, `$linkedFields`, allows you to provide a list of fields that will be sent with their values to the callback, so you can filter the results based on the values of other fields.
53+
54+
Example:
2755

2856
```php
29-
Route::get('/api/sharp/clients')
30-
->middleware('sharp_auth')
31-
->uses('MySharpApiClientController@index')
57+
SharpFormAutocompleteRemoteField::make('customer')
58+
->setRemoteCallback(function ($search, $linkedFields) {
59+
return Customer::select('id', 'name', 'email')
60+
->when(
61+
$linkedFields['country'],
62+
fn ($query) => $query->where('country_id', $linkedFields['country'])
63+
)
64+
->where('name', 'like', "%$search%")
65+
->get();
66+
}, linkedFields: ['country']);
3267
```
3368

69+
### `allowEmptySearch()`
70+
71+
This method allows to call the endpoint / callback with empty search (on first click on the field for example). It's equivalent to `setSearchMinChars(0)`.
72+
3473
### `setSearchMinChars(int $searchMinChars)`
3574

3675
Set a minimum number of character to type before performing the search.
@@ -61,7 +100,7 @@ Set the remote method to GET (default) or POST.
61100
In a remote autocomplete case, you can use this method instead of `setRemoteEndpoint` to handle a dynamic URL, based on another form field. Here's how, for example:
62101

63102
```php
64-
SharpFormAutocompleteField::make('brand', 'remote')
103+
SharpFormAutocompleteRemoteField::make('brand')
65104
->setDynamicRemoteEndpoint('/brands/{{country}}');
66105
```
67106

@@ -71,7 +110,7 @@ You may need to provide a default value for the endpoint, used when `country` (i
71110
fill the second argument:
72111

73112
```php
74-
SharpFormAutocompleteField::make('model', 'remote')
113+
SharpFormAutocompleteRemoteField::make('model')
75114
->setDynamicRemoteEndpoint(''/models/{{country}}/{{brand}}'', [
76115
'country' => 'france',
77116
'brand' => 'renault'
@@ -90,53 +129,22 @@ Set the name of the id attribute for items. This is useful :
90129
- to designate the id attribute in the remote API call return.
91130
Default: `"id"`
92131

93-
### `setListItemInlineTemplate(string $template)`
94-
### `setResultItemInlineTemplate(string $template)`
95-
Just write the template as a string, using placeholders for data like this: `{{var}}`.
96-
97-
Example:
98-
99-
```php
100-
$panel->setInlineTemplate(
101-
'Foreground: <strong>{{color}}</strong>'
102-
)
103-
```
104-
105-
The template will be used, depending on the function, to display either the list item (in the result dropdown) or the result item (meaning the valuated form input).
106-
107-
Be aware that you'll need for this to work to pass a valuated object to the Autocomplete, as data.
108-
109-
### `setListItemTemplatePath(string $listItemTemplatePath)`
110-
### `setResultItemTemplatePath(string $resultItemTemplate)`
111-
112-
Use this if you need more control: give the path of a full template, in its own file.
113-
114-
The template will be [interpreted by Vue.js](https://vuejs.org/v2/guide/syntax.html), meaning you can add data placeholders, DOM structure but also directives, and anything that Vue will parse. For instance:
132+
### `setListItemTemplate(View|string $template)`
133+
### `setResultItemTemplate(View|string $template)`
115134

116-
```vue
117-
<div v-if="show">result is {{value}}</div>
118-
<div v-else>result is unknown</div>
119-
```
120-
121-
The template will be used, depending on the function, to display either the list item (in the result dropdown) or the result item (meaning the valuated form input).
122-
123-
Be aware that you'll need for this to work to pass a valuated object to the Autocomplete, as data.
135+
The templates for the list and result items can be set in two ways: either by passing a string, or by passing a Laravel view.
124136

125-
### `setAdditionalTemplateData(array $data)`
126-
127-
Useful to add some static (or at least not instance-dependant) data to the template. For instance:
137+
Examples:
128138

129139
```php
130-
SharpFormAutocompleteRemoteField::make('brand')
131-
->setAdditionalTemplateData([
132-
'years' => [2020, 1018, 2017]
133-
]);
134-
```
135-
136-
In the template, the provided data can be used as normal:
137-
138-
```vue
139-
<div v-for="year in years"> {{ year }} </div>
140+
SharpFormAutocompleteRemoteField::make('customer')
141+
->setRemoteCallback(function ($search) {
142+
return Customer::select('id', 'name', 'email')
143+
->where('name', 'like', "%$search%")
144+
->get();
145+
})
146+
->setListItemTemplate('<div>{{$name}}</div><div><small>{{$email}}</small></div>')
147+
->setResultItemTemplate(view('my/customer/blade/view'));
140148
```
141149

142150
## Formatter

0 commit comments

Comments
 (0)