Ajax with fetch(), Polyfills & async/await

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

To make the Ajax request inside the controller, I'm going to use the fetch() function.

fetch() for AJAX

If you don't know, fetch() is a built-in function for making Ajax calls that most browsers support. So basically... pretty much all browsers except for IE 11.

If you do need to support IE 11, you have two options. First, the other library I really like for making AJAX requests is called Axios, which you can install with yarn add axios. We use it in our Vue tutorial. It also has a few more features than fetch.

Your second option is to "polyfill" fetch() so it works in all browsers. Github itself maintains a polyfill for fetch.

It's pretty simply to set up: yarn add whatwg-fetch then, in webpack.config.js, adapt your main entry to include it first:

// webpack.config.js

Encore
    // ...
-    .addEntry('app', './assets/app.js')
+    .addEntry('app', ['whatwg-fetch', './assets/app.js'])

Using URLSearchParams() & its Polyfill

Anyways, let's make the AJAX call!

We need to do send 2 query parameters on the AJAX request: the value of the search box as a q query parameter and another one called preview so that our controller knows to render the preview HTML.

To help create the query string, say const params = new URLSearchParams() and pass that an object. Inside set q to the value of the input, which we know will be event.currentTarget - to get the input Element that we attached the action to - then .value. Also pass preview set to 1.

... lines 1 - 2
export default class extends Controller {
... lines 4 - 7
onSearchInput(event) {
const params = new URLSearchParams({
q: event.currentTarget.value,
preview: 1,
});
... line 13
}
}

This URLSearchParams object is a built-in JavaScript object that helps you... basically create query strings. It is also not supported in all browsers... cough IE 11. But, it will be polyfilled automatically by Babel, as long as you have the default Encore configuration:

// webpack.config.js

Encore
    .configureBabelPresetEnv((config) => {
        config.useBuiltIns = 'usage';
        config.corejs = 3;
    })

Yea, I know, it's a little confusing. The @babel/preset-env library that Encore uses will automatically detect when you use a feature that doesn't work with the browsers that you want to support. When it detects that, it automatically adds a polyfill. This supports almost every polyfill... with fetch() being one of the few that it does not... which is why you need to add it manually.

Tip

If you want to see a list of all the polyfills added by Babel, you can add a debug flag. A report will be printed to the terminal when you build Encore:

// webpack.config.js

Encore
    .configureBabelPresetEnv((config) => {
        // ...
+        config.debug = true;
    })

Making the AJAX Request with fetch()

Ok, let's make the AJAX call: say fetch() then pass it the fancy ticks so we can create a dynamic string. We need ${this.urlValue} then a question mark and another ${} with params.toString(). That builds the query string with the & symbols.

... lines 1 - 7
onSearchInput(event) {
... lines 9 - 12
fetch(`${this.urlValue}?${params.toString()}`);
}
... lines 15 - 16

Like all Ajax libraries, fetch() returns a Promise. So if we want to get the response, we have two options. First, we can use the .then() syntax and pass a callback.

Or, we can use await... which I like because it looks simpler. Remove the .then() and instead say const response = await fetch(...).

This will wait for the AJAX call to finish and set the result to the response variable. But as soon as you use await, you must add async before whatever function that you're inside of. Yup, that makes my build happy.

... lines 1 - 7
async onSearchInput(event) {
... lines 9 - 12
const response = await fetch(`${this.urlValue}?${params.toString()}`);
... lines 14 - 15
}
... lines 17 - 18

The async is really just a marker that says that, thanks to the await, this function will now automatically return a Promise. That doesn't really matter unless you want to call this function directly and use its return value. If you were doing that, you would also need to await for this function to finish.

But... the response itself, isn't that interesting. What we really want is the response body. We can log that with console.log(await response.text()).

... lines 1 - 7
async onSearchInput(event) {
... lines 9 - 14
console.log(await response.text());
}
... lines 17 - 18

This shows off a... sort of weird thing about fetch(). When we make the Ajax call, we - of course - need to await for it to finish. But even getting the body of the response - with response.text() - is an asynchronous operation that returns a Promise. That's... kind of odd... but ok: we just need to await it.

Ok, testing time! Refresh and... type! Yes! I can already see new AJAX requests popping into the web debug toolbar! And the console is dumping the HTML.

The only problem is that this is the full HTML of the homepage... which isn't really what we want. What we want is a "fragment" of HTML that represents the search suggestions. Let's get that hooked up next and render it onto the page!

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