Search Suggestions

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

One thing we haven't done yet in stimulus is handle Ajax. Let's do that by "enhancing" the search on this page. The search here does work: it's a completely normal form that submits via a GET request. In the controller - src/Controller/ProductController.php - we read the q query parameter and use it to filter the product list.

Now, our UX team wants to make it fancier: as the user types we will render a "quick results" or "suggestions" list below the search box.

Bootstrapping the Controller & Markup

Let's get to work. Start with the Stimulus controller. So in assets/controllers/ create a new file called, how about, search-preview_controller.js.

This starts exactly like every other Stimulus controller ever: import { Controller } from 'stimulus' and export default class extends Controller.

I also like to have a connect() method so we can console.log() a message to make sure everything is... um... connected.

import { Controller } from 'stimulus';
export default class extends Controller {
connect() {
console.log('connected!');
}
}

The template for the homepage lives at templates/product/index.html.twig. Scroll down a bit... perfect. Here's the search form.

So let's think: we could add the data-controller attribute directly to the <input>. After all, we need to do something when that input changes. But we're also going to need a place to put the new search suggestions HTML.

So let's add the controller on the <div> around the input. Break this onto multiple lines and then add {{ stimulus_controller() }} passing the name of our controller: search-preview.

... lines 1 - 2
{% block body %}
... lines 4 - 37
<form>
<div
class="input-group"
{{ stimulus_controller('search-preview') }}
>
... lines 43 - 49
</div>
</form>
... lines 52 - 101
{% endblock %}

Let's check it! Find your browser, refresh and check the console. Connected!

Taking Action as the User Types

Ok: each time the user types a character into the box, we need to make an Ajax request to get a list of suggested, matching products. That means we need to add an action to the input.

Do that down here with our actions syntax: data-action="", the name of our controller - search-preview - a pound sign and then the method to call. How about: onSearchInput.

... lines 1 - 37
<form>
<div
... lines 40 - 42
<input
... lines 44 - 48
data-action="search-preview#onSearchInput"
>
</div>
</form>
... lines 53 - 104

I'm calling it onSearchInput because the default action for an input element is actually called... input! That's a native DOM event that happens whenever the value of this box changes. Basically, each time the user types a character.

Copy the method name then head into the controller. Let's replace the connect() method with onSearchInput(). Give it an event argument and console.log() that.

... lines 1 - 2
export default class extends Controller {
onSearchInput(event) {
console.log(event);
}
}

Let's check it! Go refresh and... when we type... very nice!

Generating URLs in Symfony & Passing to Stimulus with Values

Look back at the controller: src/Controller/ProductController.php, at the index() method.

This action is responsible for rendering the homepage that we're currently looking at. And it also contains the logic to filter the products based on the q search query parameter. That's because, if you look in the template, our form does not have an action attribute. This means that it will submit right back to the current URL. In other words, this submits back to the homepage, but now with the ?q= part.

So here's the plan: we're going to send the Ajax request from our Stimulus controller also to the homepage route and controller... because it already has all the search logic we need. We're also going to add a second preview query parameter. We'll use that here in a few minutes to decide if we should render the full HTML page or just the HTML for the search preview.

The point is: we're going to make the Ajax request to the homepage route. So, question: in our Stimulus controller, should we just... hard-code that URL?

We could do that. But remember! Stimulus gives us a super easy way to pass values from Symfony - like a URL that we've generated - into our controller! It's the values API.

Add static values = {} an object, and create one called url. This will be a String.

... lines 1 - 3
static values = {
url: String,
}
... lines 7 - 12

Down in the method, instead of logging the event, let's console.log(this.urlValue).

... lines 1 - 7
onSearchInput(event) {
console.log(this.urlValue);
}
... lines 11 - 12

In the template, to pass this in, we can use the 2nd argument of stimulus_controller. Pass url - that's the name of the value - and set it to the normal way that we generate routes in Twig: the path() function and the name of the route, which is... let me check... app_homepage. Paste that.

... lines 1 - 37
<form>
<div
... line 40
{{ stimulus_controller('search-preview', {
url: path('app_homepage')
}) }}
>
... lines 45 - 52
</div>
</form>
... lines 55 - 106

Let's check it! Move over and refresh. The first thing you can see, if you inspect element, is that we have the new data-search-preview-url-value="/" attribute. And as we type... nice! It correctly logs that value.

Next: let's make the Ajax call! But... what should the Ajax endpoint return? JSON? No way! Definitely HTML... but not a full page of HTML.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.4.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.12.1
        "symfony/form": "5.2.*", // v5.2.3
        "symfony/framework-bundle": "5.2.*", // v5.2.3
        "symfony/property-access": "5.2.*", // v5.2.3
        "symfony/property-info": "5.2.*", // v5.2.3
        "symfony/proxy-manager-bridge": "5.2.*", // v5.2.3
        "symfony/security-bundle": "5.2.*", // v5.2.3
        "symfony/serializer": "5.2.*", // v5.2.3
        "symfony/twig-bundle": "5.2.*", // v5.2.3
        "symfony/ux-chartjs": "^1.1", // v1.2.0
        "symfony/validator": "5.2.*", // v5.2.3
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.2.1
        "twig/intl-extra": "^3.2", // v3.2.1
        "twig/twig": "^2.12|^3.0" // v3.2.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.2.3
        "symfony/maker-bundle": "^1.27", // v1.30.0
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/stopwatch": "^5.2", // v5.2.3
        "symfony/var-dumper": "^5.2", // v5.2.3
        "symfony/web-profiler-bundle": "^5.2" // v5.2.3
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.13
        "@popperjs/core": "^2.9.1", // 2.9.1
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^5.0.0-beta2", // 5.0.0-beta2
        "core-js": "^3.0.0", // 3.8.3
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.1
        "react-dom": "^17.0.1", // 17.0.1
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "^2.0.1-phylor-6095f2a9", // 2.0.1-phylor-6095f2a9
        "stimulus-use": "^0.24.0-1", // 0.24.0-1
        "sweetalert2": "^10.13.0", // 10.14.0
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.0
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}