HTML-Returning AJAX Endpoint

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

When we type in the box, we are now making an AJAX request back to the server. The response - which we can see in the log - is the full HTML of the homepage. That's... not what we want. But... what do we want?

If you use something like React or Vue, your AJAX endpoints probably return JSON. You then use that JSON to build the HTML in JavaScript. But we're using Stimulus! And Stimulus is all about building HTML on the server.

So that's what we're going to do. But instead of a full page of HTML, we're going to return an HTML fragment: just the HTML needed for the "search suggestions" area.

Head over to ProductController and, before the return, add an if statement: if $request->query->get('preview'), then we know this is the suggestions AJAX request. Inside, render a new template: return $this->render() and call it product/_searchPreview.html.twig.

... lines 1 - 14
class ProductController extends AbstractController
{
... lines 17 - 20
public function index(Request $request, CategoryRepository $categoryRepository, ProductRepository $productRepository, Category $category = null): Response
{
... lines 23 - 28
if ($request->query->get('preview')) {
return $this->render('product/_searchPreview.html.twig', [
'products' => $products,
]);
}
... lines 34 - 40
}
... lines 42 - 58
}

The template could be called anything. The _ at the front of the name is just a nice convention: I like to use it for any templates that render only part of a page. These are sometimes called partials.

Pass the template a products variable so we can render them. Be sure to add the s at the end of products... I'll find my mistake in a minute.

Now, create the template. In product, add a new file: _searchPreview.html.twig. But we're not going to extend a base template because we don't want a base layout. We're just going to start rendering content! I'll add a <div class="list-group"> to give this some markup that looks good in Bootstrap. Then {% for product in products %} and {% endfor %}.

<div class="list-group">
{% for product in products %}
... lines 3 - 10
{% endfor %}
</div>

Inside, I want each result to be a link. Add an a with href="" {{ path() }} and the name of the route to the product page, which is app_product. This route has an id wildcard. So pass id set to product.id. I'm also going to add a few more classes for styling... then inside the a, start with the simple {{ product.name }}.

Oh, and to be extra fancy, add an {% else %}. If there are no results, render a <div class="list-group-item"> with "No results found".

... line 1
{% for product in products %}
<a
class="list-group-item list-group-item-action"
href="{{ path('app_product', { id: product.id }) }}"
>
{{ product.name }}
</a>
{% else %}
<div class="list-group-item">No results found!</div>
{% endfor %}
... lines 12 - 13

I love that: the entire search preview HTML in a simple template.

To see this, go back to the homepage but add a ?preview=1 to the URL. And... oh! Variable products does not exist. Because... in the controller, I forgot my "s".

Now... much better. I mean, it looks terrible here, but that's just because this page doesn't have any CSS. Head back to the homepage.

Adding the AJAX HTML to the Target

The last step in Stimulus is to dump the HTML from the AJAX endpoint onto the page. To control exactly where it goes, let's add a new target element. In index.html.twig, I want the content to go right below the input. Add a <div>... with class="search-preview". That's a class that already lives in our CSS that will help style things.

To make this a target, we need data-search-preview-target="" and... call the new target, how about, result. It doesn't need any content by default.

... lines 1 - 2
{% block body %}
... lines 4 - 37
<form>
... lines 39 - 53
<div
class="search-preview"
data-search-preview-target="result"
></div>
... line 58
</form>
... lines 60 - 109
{% endblock %}

Over in our Stimulus controller, set up the target: static targets = [] an array with result inside.

... lines 1 - 2
export default class extends Controller {
... lines 4 - 7
static targets = ['result'];
... lines 9 - 18
}

Below, set the inner HTML on this. Copy the await response.text and replace it with this.resultTarget.innerHTML equals await response.text().

... lines 1 - 9
async onSearchInput(event) {
... lines 11 - 16
this.resultTarget.innerHTML = await response.text();
}
... lines 19 - 20

Done! Let's go try it! I'll click to go back to the homepage... just to clear the search entirely. Moment of truth: type. Ha! We got it! And if I type something nutty, no results found. It's alive!

Making the Search Preview Prettier

Let's celebrate by making it prettier.

Back over in the template - _searchPreview.html.twig - instead of rendering just the name, I'll paste in some markup. You can copy this from the code block on this page... but it's pretty basic.

... line 1
{% for product in products %}
<a
... lines 4 - 5
>
<div class="d-flex align-items-center justify-content-between">
<div>
<img
alt="{{ product.name }}"
src="{{ asset('/uploads/products/'~product.imageFilename) }}"
class="rounded"
>
<span class="px-1">{{ product.name }}</span>
</div>
<em>{{ product.priceString|format_currency('USD') }}</em>
</div>
</a>
... lines 19 - 20
{% endfor %}
... lines 22 - 23

Move over and try it again. I actually didn't even need to refresh. Now type. Ah! Gorgeous! And you can click any of these to see that product.

Look back at the Stimulus controller. This whole feature took about 15 lines of JavaScript, only a couple lines of PHP and a very simple template that renders the results.

But it's not perfect yet. If I click off of the search area... it doesn't go away! We really need that to close. How can we do that? The easiest way is by leveraging a third party library that's full of behaviors for Stimulus, like "click outside" and "debounce". That's next.

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
    }
}