Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions demo/app/Sharp/TestForm/TestForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 16 additions & 6 deletions docs/guide/form-fields/autocomplete.md
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -129,22 +128,33 @@ 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('<div>{{$name}}</div><div><small>{{$email}}</small></div>')
->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) {
return Customer::select('id', 'name', 'email')
->where('name', 'like', "%$search%")
->get();
})
->setListItemTemplate('<div>{{$name}}</div><div><small>{{$email}}</small></div>')
->setResultItemTemplate(view('my/customer/blade/view'));
->setListItemTemplate(fn ($customer) => '<div>{{$customer->getFullName()}}</div>');
```

## Formatter
Expand Down
4 changes: 1 addition & 3 deletions src/Form/Fields/Formatters/AutocompleteRemoteFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
30 changes: 22 additions & 8 deletions src/Form/Fields/Utils/SharpFormAutocompleteCommonField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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),
Expand All @@ -40,31 +42,43 @@ 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);
}

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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Form/Fields/Utils/SharpFormFieldWithOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 2 additions & 5 deletions src/Http/Controllers/Api/ApiFormAutocompleteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)),
]);
}

Expand Down
112 changes: 112 additions & 0 deletions tests/Http/Api/ApiFormAutocompleteControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading