Symfony 4 | Suche mit typeahead.js

Eine Suche im Controller ist schön. Eine Ajax Suche im Controller ist schöner. Eine Suche mit typeahead im Controller ist am schönsten….

Typeahead

Typeahead is a feature of computers and software (and some typewriters) that enables users to continue typing regardless of program or computer operation—the user may type in whatever speed is desired, and if the receiving software is busy at the time it will be … (Quelle: Wikipedia)

Es geht hier also um eine Autovervollständigungsuche.

Installation

EDIT: Leider hat das ganze über Webpack NICHT funktioniert. Wenn es jemand schafft, bitte meldet euch in den Kommentaren. 

Wir nutzen wie üblich yarn um unsere JS Libraries einzubinden. In unserem Fall nutzen wir 

yarn add imports-loader
yarn add jquery-typeahead

und „requirern“ dies in unserer app.js like so

require('jquery-typeahead');

Javascript / jQuery

Grundsätzlich benötige ich die Suche auch nur im Backend, daher ist es vermutlich aus Performance gründen auch ganz gut wenn ich das nicht in meiner main/app.js habe.

Laden wir uns also die Lib bei Github herunter und binden Sie mit dem script Tag ein:

<script type="text/javascript" src="{{ asset('js/typeahead.bundle.js') }}"></script>

In der bundle.js ist bereits Bloodhound mit enthalten. Wenn Ihr keine engine braucht, ladet euch einfach nur die typeahead.js runter. Nun habt Ihr zwei Möglichkeiten, entweder das ganze direkt im Template einbinden oder die in eine .js Datei auslagern. Dabei ist zu beachten, dass Ihr eine globale URL braucht (zum Beispiel im Footer eurer base.html.twig Datei).

Der Vorteil beim einbinden direkt im Template ist, dass Ihr URLs per Twig generieren könnt:

<script>
$(document).ready(function () {

    var engine = new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.obj.whitespace,
        queryTokenizer: Bloodhound.tokenizers.obj.whitespace('name'),
        remote: {
            url: '{{ path('adresses_search', {'query': 'QUERY'})}}',
            wildcard: 'QUERY'
        }
    });

    $('#typeaheader').typeahead(
        {
            highlight: true,
            minLength: 3
        },
        {
            name: 'term',
            source: engine,
            limit: 10,
            templates: {
                empty: '<div class="suggestion"><span class="suggestion_text no-underline">   - keine Treffer -   </span></div>',
                suggestion: function (el) {

                    return '<a class="suggestion_text no-underline" href="'+el.url+'"><div class="suggestion">' + el.firstname +' '+ el.lastname + '</div></a>'
                }
            }
        }

    );

});
</script>

Sprich bei der Bloodhound remote:url übergebe ich eine URL durch eine Twig Funktion generiert. Also selbst wenn sich hier mal die Route ändert (und nicht der Name der Action) seid Ihr hier auf der sicheren Seite.

Nun ist alles für das Input Feld vorbereitet, welches wir nun ins gleiche Template packen.

<div class="row t-search">
        <div class="col-12">
            <div class="form-group">
                <input class="form-control" id="typeaheader" style="width: 100%;" class="typeahead" type="text" placeholder="{{ 'form.order.firstname.label'|trans }}, {{ 'form.order.lastname.label'|trans }}, {{ 'form.address.city.label'|trans }}">
            </div>
        </div>
    </div>

Da wir typeahead auf die ID typeaheader gelegt haben können wir nun bereits den ersten Call machen und schauen was passiert.

keine Treffer (500er)

Was fehlt? Richtig. Wir haben noch keine search Action integriert.

Controller

/**
* @Route("/admin/adresses/search/{query}", name="adresses_search", methods={"POST", "GET"})
*
* @param $query
* @return JsonResponse
*/
public function search(Request $request, $query)
{
$data = $this->getDoctrine()->getManager()->getRepository(Adresses::class)->findByName($query);

foreach ($data as $key=>$datas){
$data[$key]['url']= $this->generateUrl('adresses_edit', ['id' => $datas['id']]);
}

$normalizers = [
new ObjectNormalizer()
];
$encoders =  [
new JsonEncoder()
];
$serializer = new Serializer($normalizers, $encoders);
$data = $serializer->serialize($data, 'json');
return new JsonResponse($data, 200, [], true);
}

Wir nutzen wie üblich die Annotations fürs routen und legen dort fest das wir einen Parameter $query erwarten. Mit diesem Suchstring lassen wir uns vom Adresses Repo dann die Daten zurückgeben

Repository

/**
* @param $value
*
* @return array
*/
public function findByName($value)
    {
        return $this->createQueryBuilder('a')
                    ->select('o.firstname, o.lastname,  a.id')
                    ->leftJoin('a.orderId','o')
                    ->where('o.firstname like :query')
                    ->orWhere('o.lastname like :query')
                    ->setParameter('query', "%". $value ."%")
                    ->getQuery()->getResult();
    }

Je nach belieben der Suchleistung und Feinheit könnt Ihr natürlich das Query anpassen. Bei meiner DB mit einer 5-stelligen Zahl an Einträgen ist es kein Problem mit einem LIKE zu suchen.

Danach iterieren wir nochmal über das zurückgegebene Array und generieren uns die URL. Hier wäre später sicherlich ein Service hilfreich, da dies nicht der einzige Punkt sein wird an dem ich eine URL der Adresse benötige.

Ergebnis

Und das war es auch schon, Ihr solltet nun ein Ergebnis bei der Suche sehen.

Typeahead Suchergebnisse

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.