From 96ae97fa74e33997f857f96bbda1cacb3b492aea Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 13 May 2025 17:10:32 +0200 Subject: [PATCH 1/6] Add $item in autocomplete template data + allow closure --- .../AutocompleteRemoteFormatter.php | 4 +-- .../SharpFormAutocompleteCommonField.php | 30 ++++++++++++++----- .../Utils/SharpFormFieldWithOptions.php | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) 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..0804877fd 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->resultItemTemplate)) { + 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..1a8145295 100644 --- a/src/Form/Fields/Utils/SharpFormFieldWithOptions.php +++ b/src/Form/Fields/Utils/SharpFormFieldWithOptions.php @@ -20,7 +20,7 @@ protected static function formatOptions(array|Collection $options, string $idAtt 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 From c33f0daf51ff9c615b79c42eb0cd3730714795f5 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 20 May 2025 10:56:18 +0200 Subject: [PATCH 2/6] Add tests --- .../SharpFormAutocompleteCommonField.php | 2 +- .../Api/ApiFormAutocompleteControllerTest.php | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/Form/Fields/Utils/SharpFormAutocompleteCommonField.php b/src/Form/Fields/Utils/SharpFormAutocompleteCommonField.php index 0804877fd..691a7f8d7 100644 --- a/src/Form/Fields/Utils/SharpFormAutocompleteCommonField.php +++ b/src/Form/Fields/Utils/SharpFormAutocompleteCommonField.php @@ -58,7 +58,7 @@ public function setResultItemTemplate(View|string|Closure $template): self protected function renderListItem($data): string { - if (is_callable($this->resultItemTemplate)) { + if (is_callable($this->listItemTemplate)) { return ($this->listItemTemplate)($data); } diff --git a/tests/Http/Api/ApiFormAutocompleteControllerTest.php b/tests/Http/Api/ApiFormAutocompleteControllerTest.php index ec2abbf03..12c753b4f 100644 --- a/tests/Http/Api/ApiFormAutocompleteControllerTest.php +++ b/tests/Http/Api/ApiFormAutocompleteControllerTest.php @@ -249,6 +249,78 @@ public function buildFormFields(FieldsContainer $formFields): void ]); }); +it('passes the full object 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 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(); From 2270b4e1e71f0797a7671ea7093d5f154f6f1cc2 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 20 May 2025 11:13:20 +0200 Subject: [PATCH 3/6] Add test for object / model case --- .../Api/ApiFormAutocompleteController.php | 7 +--- .../Api/ApiFormAutocompleteControllerTest.php | 42 ++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) 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 12c753b4f..9cfd32254 100644 --- a/tests/Http/Api/ApiFormAutocompleteControllerTest.php +++ b/tests/Http/Api/ApiFormAutocompleteControllerTest.php @@ -249,7 +249,7 @@ public function buildFormFields(FieldsContainer $formFields): void ]); }); -it('passes the full object in the item variable in the template', function () { +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 @@ -285,6 +285,46 @@ public function buildFormFields(FieldsContainer $formFields): void ]); }); +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 { From 848273e8b239fc61a25851096e1b1a8eeb9f9d63 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 May 2025 11:34:30 +0200 Subject: [PATCH 4/6] convert to array for select case --- src/Form/Fields/Utils/SharpFormFieldWithOptions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Form/Fields/Utils/SharpFormFieldWithOptions.php b/src/Form/Fields/Utils/SharpFormFieldWithOptions.php index 1a8145295..42fe541b1 100644 --- a/src/Form/Fields/Utils/SharpFormFieldWithOptions.php +++ b/src/Form/Fields/Utils/SharpFormFieldWithOptions.php @@ -16,7 +16,7 @@ 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 From 79df10250162884b73d76b3a01716c521db92328 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 May 2025 11:35:06 +0200 Subject: [PATCH 5/6] update test form --- demo/app/Sharp/TestForm/TestForm.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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, From 42e96033603ca0d3c34c6d34ffb996490dbe7682 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 20 May 2025 15:08:30 +0200 Subject: [PATCH 6/6] Doc --- docs/guide/form-fields/autocomplete.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) 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