diff --git a/demo/app/Sharp/TestForm/TestForm.php b/demo/app/Sharp/TestForm/TestForm.php
index 7bc3c20e2..6c677b967 100644
--- a/demo/app/Sharp/TestForm/TestForm.php
+++ b/demo/app/Sharp/TestForm/TestForm.php
@@ -2,6 +2,7 @@
namespace App\Sharp\TestForm;
+use App\Models\User;
use Code16\Sharp\Form\Eloquent\Uploads\Transformers\SharpUploadModelFormAttributeTransformer;
use Code16\Sharp\Form\Fields\Editor\Uploads\SharpFormEditorUpload;
use Code16\Sharp\Form\Fields\SharpFormAutocompleteListField;
@@ -61,8 +62,8 @@ public function buildFormFields(FieldsContainer $formFields): void
SharpFormAutocompleteRemoteField::make('autocomplete_remote')
->setLabel('Autocomplete remote')
->setRemoteSearchAttribute('query')
- ->setListItemTemplate('{{ $name }}')
- ->setResultItemTemplate('{{ $name }} ({{ $id }})')
+ ->setListItemTemplate('{{ $item["name"] }}')
+ ->setResultItemTemplate('{{ $item["name"] }} ({{ $item["id"] }})')
// ->setReadOnly()
->setRemoteEndpoint(route('sharp.autocompletes.users.index'))
// ->setRemoteCallback(function ($search, $data) {
@@ -400,6 +401,7 @@ protected function findSingle()
}
return $this
+ ->setCustomTransformer('autocomplete_remote', fn ($value) => User::find($value))
->setCustomTransformer('upload', (new SharpUploadModelFormAttributeTransformer())->dynamicInstance())
->setCustomTransformer('html', fn () => [
'name' => fake()->name,
diff --git a/docs/guide/form-fields/autocomplete.md b/docs/guide/form-fields/autocomplete.md
index ed782ccc7..78ed98099 100644
--- a/docs/guide/form-fields/autocomplete.md
+++ b/docs/guide/form-fields/autocomplete.md
@@ -106,8 +106,7 @@ SharpFormAutocompleteRemoteField::make('brand')
In this example, the `{{country}}` placeholder will be replaced by the value of the `country` form field. You can define multiple replacements if necessary.
-You may need to provide a default value for the endpoint, used when `country` (in our example) is not valued (without default, the autocomplete field will be displayed as disabled). To do that,
-fill the second argument:
+You may need to provide a default value for the endpoint, used when `country` (in our example) is not valued (without default, the autocomplete field will be displayed as disabled). To do that, fill the second argument:
```php
SharpFormAutocompleteRemoteField::make('model')
@@ -129,13 +128,25 @@ Set the name of the id attribute for items. This is useful :
- to designate the id attribute in the remote API call return.
Default: `"id"`
-### `setListItemTemplate(View|string $template)`
-### `setResultItemTemplate(View|string $template)`
+### `setListItemTemplate(View|string|Closure $template)`
+### `setResultItemTemplate(View|string|Closure $template)`
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.
Examples:
+```php
+SharpFormAutocompleteRemoteField::make('customer')
+ ->setRemoteEndpoint('/api/customers')
+ ->setListItemTemplate('
{{$name}}
{{$email}}
')
+ ->setResultItemTemplate(view('my/customer/blade/view'));
+```
+
+Note that the template can access to every attribute of the item (which will be sent as JSON by the API endpoint, and cast into an array) as a variable. In this example, we assume that the API endpoint returns an array of objects with `id`, `name` and `email` attributes.
+
+There is a third way to set the templates, by passing a Closure. This is **only suitable in one case: a remote autocomplete with a callback**. The closure will receive the unchanged item as a parameter (it’s useful when this item is an object, like a Model for instance), and must return a string.
+Here’s a simple example:
+
```php
SharpFormAutocompleteRemoteField::make('customer')
->setRemoteCallback(function ($search) {
@@ -143,8 +154,7 @@ SharpFormAutocompleteRemoteField::make('customer')
->where('name', 'like', "%$search%")
->get();
})
- ->setListItemTemplate('{{$name}}
{{$email}}
')
- ->setResultItemTemplate(view('my/customer/blade/view'));
+ ->setListItemTemplate(fn ($customer) => '{{$customer->getFullName()}}
');
```
## Formatter
diff --git a/src/Form/Fields/Formatters/AutocompleteRemoteFormatter.php b/src/Form/Fields/Formatters/AutocompleteRemoteFormatter.php
index aa214c6a8..2106ac4e2 100644
--- a/src/Form/Fields/Formatters/AutocompleteRemoteFormatter.php
+++ b/src/Form/Fields/Formatters/AutocompleteRemoteFormatter.php
@@ -14,13 +14,11 @@ class AutocompleteRemoteFormatter extends SharpFieldFormatter
*/
public function toFront(SharpFormField $field, $value)
{
- $value = ArrayConverter::modelToArray($value);
-
if (is_null($value)) {
return null;
}
- if (is_array($value)) {
+ if (is_array(ArrayConverter::modelToArray($value))) {
return $field->itemWithRenderedTemplates($value);
}
diff --git a/src/Form/Fields/Utils/SharpFormAutocompleteCommonField.php b/src/Form/Fields/Utils/SharpFormAutocompleteCommonField.php
index 4c25b672d..691a7f8d7 100644
--- a/src/Form/Fields/Utils/SharpFormAutocompleteCommonField.php
+++ b/src/Form/Fields/Utils/SharpFormAutocompleteCommonField.php
@@ -2,6 +2,8 @@
namespace Code16\Sharp\Form\Fields\Utils;
+use Closure;
+use Code16\Sharp\Utils\Transformers\ArrayConverter;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Blade;
@@ -15,17 +17,17 @@ trait SharpFormAutocompleteCommonField
protected string $mode;
protected string $itemIdAttribute = 'id';
protected ?array $dynamicAttributes = null;
- protected View|string|null $listItemTemplate = null;
- protected View|string|null $resultItemTemplate = null;
+ protected View|string|Closure|null $listItemTemplate = null;
+ protected View|string|Closure|null $resultItemTemplate = null;
- public function itemWithRenderedTemplates(array $item): array
+ public function itemWithRenderedTemplates($item): array
{
$resultItem = $this->resultItemTemplate
? ['_htmlResult' => $this->renderResultItem($item)]
: [];
return [
- ...$item,
+ ...ArrayConverter::modelToArray($item),
'_html' => $this->listItemTemplate
? $this->renderListItem($item)
: ($item['label'] ?? $item[$this->itemIdAttribute] ?? null),
@@ -40,22 +42,28 @@ public function setItemIdAttribute(string $itemIdAttribute): self
return $this;
}
- public function setListItemTemplate(View|string $template): self
+ public function setListItemTemplate(View|string|Closure $template): self
{
$this->listItemTemplate = $template;
return $this;
}
- public function setResultItemTemplate(View|string $template): self
+ public function setResultItemTemplate(View|string|Closure $template): self
{
$this->resultItemTemplate = $template;
return $this;
}
- public function renderListItem(array $data): string
+ protected function renderListItem($data): string
{
+ if (is_callable($this->listItemTemplate)) {
+ return ($this->listItemTemplate)($data);
+ }
+
+ $data = ['item' => $data, ...ArrayConverter::modelToArray($data)];
+
if (is_string($this->listItemTemplate)) {
return Blade::render($this->listItemTemplate, $data);
}
@@ -63,8 +71,14 @@ public function renderListItem(array $data): string
return $this->listItemTemplate->with($data)->render();
}
- public function renderResultItem(array $data): string
+ protected function renderResultItem($data): string
{
+ if (is_callable($this->resultItemTemplate)) {
+ return ($this->resultItemTemplate)($data);
+ }
+
+ $data = ['item' => $data, ...ArrayConverter::modelToArray($data)];
+
if (is_string($this->resultItemTemplate)) {
return Blade::render($this->resultItemTemplate, $data);
}
diff --git a/src/Form/Fields/Utils/SharpFormFieldWithOptions.php b/src/Form/Fields/Utils/SharpFormFieldWithOptions.php
index f837698fa..42fe541b1 100644
--- a/src/Form/Fields/Utils/SharpFormFieldWithOptions.php
+++ b/src/Form/Fields/Utils/SharpFormFieldWithOptions.php
@@ -16,11 +16,11 @@ protected static function formatOptions(array|Collection $options, string $idAtt
$options = collect($options);
$firstOption = ArrayConverter::modelToArray($options->first());
- $format ??= fn ($option) => $option;
+ $format ??= fn ($option) => ArrayConverter::modelToArray($option);
if (is_array($firstOption) && isset($firstOption[$idAttribute])) {
// We assume that we already have ["id", "label"] in this case
- return $options->map(fn ($option) => $format(ArrayConverter::modelToArray($option)))->values()->all();
+ return $options->map(fn ($option) => $format($option))->values()->all();
}
// Simple [key => value] array case
diff --git a/src/Http/Controllers/Api/ApiFormAutocompleteController.php b/src/Http/Controllers/Api/ApiFormAutocompleteController.php
index a5c5c8f60..8ea8ee7dc 100644
--- a/src/Http/Controllers/Api/ApiFormAutocompleteController.php
+++ b/src/Http/Controllers/Api/ApiFormAutocompleteController.php
@@ -11,7 +11,6 @@
use Code16\Sharp\Http\Controllers\Api\Commands\HandlesInstanceCommand;
use Code16\Sharp\Http\Controllers\Api\Embeds\HandlesEmbed;
use Code16\Sharp\Utils\Entities\ValueObjects\EntityKey;
-use Code16\Sharp\Utils\Transformers\ArrayConverter;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Route;
@@ -55,11 +54,9 @@ public function index(EntityKey $entityKey, string $autocompleteFieldKey)
->toArray()
: null;
- $data = collect($callback(request()->input('search'), $formData))
- ->map(fn ($record) => ArrayConverter::modelToArray($record));
-
return response()->json([
- 'data' => $data->map(fn ($item) => $field->itemWithRenderedTemplates($item)),
+ 'data' => collect($callback(request()->input('search'), $formData))
+ ->map(fn ($item) => $field->itemWithRenderedTemplates($item)),
]);
}
diff --git a/tests/Http/Api/ApiFormAutocompleteControllerTest.php b/tests/Http/Api/ApiFormAutocompleteControllerTest.php
index ec2abbf03..9cfd32254 100644
--- a/tests/Http/Api/ApiFormAutocompleteControllerTest.php
+++ b/tests/Http/Api/ApiFormAutocompleteControllerTest.php
@@ -249,6 +249,118 @@ public function buildFormFields(FieldsContainer $formFields): void
]);
});
+it('passes the full data in the item variable in the template', function () {
+ fakeFormFor('person', new class() extends PersonForm
+ {
+ public function buildFormFields(FieldsContainer $formFields): void
+ {
+ $formFields->addField(
+ SharpFormAutocompleteRemoteField::make('autocomplete_field')
+ ->setRemoteMethodPOST()
+ ->setListItemTemplate('{{ $item["name"] }}, {{ $item["job"] }}')
+ ->setRemoteEndpoint('/my/endpoint')
+ );
+ }
+ });
+
+ Route::post('/my/endpoint', fn () => [
+ ['id' => 1, 'name' => 'John', 'job' => 'actor'],
+ ['id' => 2, 'name' => 'Jane', 'job' => 'producer'],
+ ]);
+
+ $this
+ ->postJson(route('code16.sharp.api.form.autocomplete.index', [
+ 'entityKey' => 'person',
+ 'autocompleteFieldKey' => 'autocomplete_field',
+ ]), [
+ 'endpoint' => '/my/endpoint',
+ 'search' => 'my search',
+ ])
+ ->assertOk()
+ ->assertJson([
+ 'data' => [
+ ['id' => 1, 'name' => 'John', 'job' => 'actor', '_html' => 'John, actor'],
+ ['id' => 2, 'name' => 'Jane', 'job' => 'producer', '_html' => 'Jane, producer'],
+ ],
+ ]);
+});
+
+it('allows objects / models as the item variable in the template in a callback case', function () {
+ fakeFormFor('person', new class() extends PersonForm
+ {
+ public function buildFormFields(FieldsContainer $formFields): void
+ {
+ $formFields->addField(
+ SharpFormAutocompleteRemoteField::make('autocomplete_field')
+ ->setRemoteCallback(function () {
+ return [
+ new class()
+ {
+ public int $id = 1;
+
+ public function getNameAndJob(): string
+ {
+ return 'John, actor';
+ }
+ },
+ ];
+ })
+ ->setListItemTemplate('{{ $item->getNameAndJob() }}')
+ );
+ }
+ });
+
+ $this
+ ->postJson(route('code16.sharp.api.form.autocomplete.index', [
+ 'entityKey' => 'person',
+ 'autocompleteFieldKey' => 'autocomplete_field',
+ ]), [
+ 'search' => 'my search',
+ ])
+ ->assertOk()
+ ->assertJson([
+ 'data' => [
+ ['id' => 1, '_html' => 'John, actor'],
+ ],
+ ]);
+});
+
+it('allows Closure as item template', function () {
+ fakeFormFor('person', new class() extends PersonForm
+ {
+ public function buildFormFields(FieldsContainer $formFields): void
+ {
+ $formFields->addField(
+ SharpFormAutocompleteRemoteField::make('autocomplete_field')
+ ->setRemoteMethodPOST()
+ ->setListItemTemplate(fn ($data) => $data['name'].', '.$data['job'])
+ ->setRemoteEndpoint('/my/endpoint')
+ );
+ }
+ });
+
+ Route::post('/my/endpoint', fn () => [
+ ['id' => 1, 'name' => 'John', 'job' => 'actor'],
+ ['id' => 2, 'name' => 'Jane', 'job' => 'producer'],
+ ]);
+
+ $this
+ ->postJson(route('code16.sharp.api.form.autocomplete.index', [
+ 'entityKey' => 'person',
+ 'autocompleteFieldKey' => 'autocomplete_field',
+ ]), [
+ 'endpoint' => '/my/endpoint',
+ 'search' => 'my search',
+ ])
+ ->assertOk()
+ ->assertJson([
+ 'data' => [
+ ['id' => 1, 'name' => 'John', 'job' => 'actor', '_html' => 'John, actor'],
+ ['id' => 2, 'name' => 'Jane', 'job' => 'producer', '_html' => 'Jane, producer'],
+ ],
+ ]);
+});
+
it('fails if field is missing', function () {
$this->withoutExceptionHandling();