Open ghost opened 3 years ago
I have a similar requirement. As a workaround, I thought about adding a non entity related field with a bit of Javascript which would populate another field (hidden?) when the "external drop down" is modified. I haven't add the time to do it yet (and still struggling a bit with some of EA features...). @a-leclerc let me know if you got anywhere please? PS: It would be great to start collecting these recipes in a doc...
Hi,
I did it :)
I created a new Field : /src/Admin/Field/AddressField.php
<?php
namespace App\Admin\Field;
use App\Form\Type\AddressGouvType;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\CollectionField;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
final class AddressField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null): self
{
return (new self())
->setProperty($propertyName)
//->setLabel($label)
->setTemplatePath('admin/field/address.html.twig')
->setFormType(AddressGouvType::class)
->addCssClass('field-address')
->addCssFiles('https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@9.0.0/dist/css/autoComplete.min.css')
->addJsFiles('https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@9.0.0/dist/js/autoComplete.min.js')
->addJsFiles('admin/field/address/field-address.js')
->addCssFiles('admin/field/address/field-address.css')
;
}
}
which is related to : /src/Form/Type/AddressGouvType.php
<?php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Form\FormBuilderInterface;
class AddressGouvType extends AbstractType
{
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function getParent()
{
return TextType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'finder_callback' => function(string $query) {
},
'attr' => [
'class' => 'js-field-address-autocomplete',
'data-autocomplete-url' => $this->router->generate('api_address_search')
]
]);
}
}
Next, I've created a js file : /public/admin/field/field-address.js
new autoComplete({
selector: ".js-field-address-autocomplete",
placeHolder: "Cherchez une adresse...",
data: {
src: async () => {
let query = document.querySelector(".js-field-address-autocomplete").value;
const source = await fetch("/api/address/search/?q=" + query);
const data = await source.json();
return data;
},
key: ["address"],
cache: false
},
trigger: {
event: ["input", "focus"]
},
resultsList: {
noResults: (list, query) => {
console.log(list);
// Create "No Results" message list element
const message = document.createElement("li");
message.setAttribute("class", "no_result");
// Add message text content
message.innerHTML = `<span>Aucun résultat pour "${query}"</span>`;
// Add message list element to the list
list.appendChild(message);
},
},
resultItem: {
highlight: {
render: true
}
},
onSelection: (feedback) => {
document.querySelector(".js-field-address-autocomplete").blur();
// Prepare User's Selected Value
const selection = feedback.selection.value[feedback.selection.key];
// Render selected choice to selection div
//document.querySelector(".selection").innerHTML = selection;
// Replace Input value with the selected value
document.querySelector(".js-field-address-autocomplete").value = selection;
// Console log autoComplete data feedback
//console.log(feedback);
}
});
And custom CSS : /public/admin/field/field-address.css
#autoComplete_list {
position: relative;
top: 10px;
}
.autoComplete_result {
font-size: 0.8rem;
}
li.no_result {
list-style-type: none;
padding: 0.5rem;
}
.js-field-address-autocomplete {
max-width: 100% !important;
}
I created also an API : /src/Controller/Api/External/AddressController.php
<?php
namespace App\Controller\Api\External;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @Route("/api", name="api_")
*/
class AddressController extends AbstractController
{
private $client;
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
/**
* @Route("/address/search/", methods="GET", name="address_search")
*/
public function search(Request $request): ?string
{
$request = $request->query->get('q');
$response = $this->client->request(
'GET',
'https://xxx/search/?q=' . urlencode( $request )
);
$response = $response->toArray();
$json = $response['features'];
$output = [];
if ( !empty( $json ) ) {
foreach ( $json as $property ) {
$address = [
$property['properties']['name'],
$property['properties']['postcode'],
$property['properties']['city'],
$property['geometry']['coordinates'][0],
$property['geometry']['coordinates'][1]
];
$output[] = [
'address' => implode(', ', $address),
];
}
}
$response = new Response(
json_encode($output),
Response::HTTP_OK,
['content-type' => 'application/json']
);
$response->send();
}
}
And I add my field in my admin panel : /src/Controller/Admin/BusinessCrubController.php
<?php
namespace App\Controller\Admin;
use App\Admin\Field\AddressField;
use App\Entity\Business;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\SlugField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TelephoneField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Field\UrlField;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
class BusinessCrudController extends AbstractCrudController
{
...
public function configureFields(string $pageName): iterable
{
...
yield AddressField::new('address', 'Adresse')
->setHelp('Commencez à taper pour trouver l\'adresse.')
->hideOnIndex()
->hideOnDetail()
;
...
}
}
I've used autoComplete vanilla for autocomplete javascript library.
Hope it will help you @Schyzophrenic
Man, that looks really good! Let me digest it and give it a try! I am not too familiar with the Promess but I should get my head around it! Thanks a lot for sharing, I am sure this is going to be very handy!
Hello again @a-leclerc , I finally have the time to look into this. As I was integrating and adapting to my use case I noticed you didn't put the file admin/field/address.html.twig
.
Would you be kind enough to share it?
Thank you again, this is going to save me a lot of time!
Hi, yes excuse me :) here is the file :
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{% set render_as_html = field.customOptions.get('renderAsHtml') %}
{% if ea.crud.currentAction == 'detail' %}
<span title="{{ field.value }}">
{{ render_as_html ? field.formattedValue|raw|nl2br : field.formattedValue|nl2br }}
</span>
{% else %}
<span title="{{ field.value }}">
{{ render_as_html ? field.formattedValue|raw : field.formattedValue|striptags }}
</span>
{% endif %}
Thanks @a-leclerc ! This is actually exactly the same as the one I wrote so I am going to dig deeper why it is not working! (First time I use the vanilla autocomplete so I assume I didn't interface the json and the lib properly). Thank you again, I'll report back on my progress ;)
Have you any error message ?
Have you any error message ?
So actually, I have reduced the complexity to add it back in gradually (and help me understand better). I removed the Ajax call now but still have no results. It seems to be due to the position absolute of #autocomplete_List.
I see you added the below, but it is not taken into account for some reason.
#autoComplete_list {
position: relative !important;
top: 10px;
}
When I remove the absolute attribute in the chrome editor, I see the results just fine.
A bit of an edit here: It seems the issue lives in the #autoComplete_list class defined by vanilla autocomplete as I wish the results to be displayed on top of the form. In order to do this, I need to remove the top, left and right attribute of the associate css class.
So far, the only way I found was to store a version of the autocomplete css locally and modify it, not ideal.
Re-reading a code is always useful... After replacing ->addCssClass by ->addCssFiles, it works much better.... Continuing with the integration.
@a-leclerc Mission accomplished, thank you for the great help!
Hey @a-leclerc . I just updated EasyAdmin to the latest version and this is now generating a javascript error.
Uncaught TypeError: Cannot read property 'setAttribute' of null at e.value (autoComplete.min.js:1) at new e (autoComplete.min.js:1) at igdb-game-finder.js:3
I am looking into it but let me know if you're facing the same!
Edit: It is actually quite simple (and written in the upgrade notes): Just encapsulate the function into the DOMContentLoaded listener as the scripts are now loaded in the head rather than the end of the body
document.addEventListener('DOMContentLoaded', () => {
// Your function
}
For those looking at this thread, this is now broken again after migration to 3.5 with
Uncaught TypeError: Cannot read properties of null (reading 'setAttribute')
This seems to be due to the class parameters not being written in the form for some reason. This is probably due to the upgrade to Bootstrap 5 and the associated twig templates, but to be confirmed
I fixed my issue via #4643
thank you @a-leclerc for your solution, it works very well, however I would like to know if there is a way to use a select (or some thing else) instead of a text field, the goal is to search a string and display it in the field but what we save is an id (which is returned by the API). Currently I managed the solution to retrieve the id and put it in an hidden field but it's impossible to persist it instead of the text field used for the research. thank you in advance for your answer
Very nice Ghost thank you !
Hi,
I'd like to know if it's possible to create autocomplete field with external datas. I'have an external API, and I'd like to populate my field with theses datas.
Best,