Using the fetch API

Catch the pager's clicks with one listener on the pager, cancel them with preventDefault, fetch the partial's HTML with fetch and response.text, and put it in the page with innerHTML. The links keep working when JavaScript doesn't run.

Journeyman · JavaScript · ASP.NET Core · Updated

After this lesson you can

  • Fetch a partial view's HTML with fetch and response.text, and swap it into the page
  • Intercept link clicks with one listener and preventDefault, keeping the links working without JavaScript

Key takeaways

  • response.text() reads the body as HTML, and grid.innerHTML = ... replaces what's inside the grid with it.
  • One listener on the pager hears every link's click as it bubbles up, and event.target.closest('a[data-page]') finds the link. That's event delegation.
  • event.preventDefault() stops the browser following the link; if the fetch fails, fall back to window.location.href = link.href.
  • data- attributes hold your own values, and JavaScript reads them through dataset: data-page="2" is link.dataset.page, the string '2'.

The last piece is the JavaScript that ties the pager to the new grid action.

Marking up the pager

The pager in Views/Products/Index.cshtml gets an id, and each item and link a data-page attribute saying which page it shows:

<nav aria-label="Product pages" class="mt-4">
    <ul class="pagination" id="product-pager">
        @for (int i = 1; i <= pageCount; i++)
        {
            <li class="page-item @(i == page ? "active" : "")" data-page="@i">
                <a class="page-link" asp-action="Index" asp-route-page="@i" data-page="@i">@i</a>
            </li>
        }
    </ul>
</nav>

@section Scripts {
    <script src="~/js/products.js" asp-append-version="true"></script>
}
  • Attributes starting with data- are for your own use. JavaScript reads them through dataset: data-page="2" is link.dataset.page, the string '2'.
  • The links' href is still /products?page=2, so they work without JavaScript.
  • The Scripts section puts the script at the end of the layout, after the page's HTML, so the elements exist by the time it runs. asp-append-version adds a version number to the address, so browsers fetch the new file whenever it changes.

The script

wwwroot/js/products.js:

const grid = document.getElementById('product-grid');
const pager = document.getElementById('product-pager');

async function showPage(page) {
    try {
        const response = await fetch(`/products/grid?page=${page}`);
        if (!response.ok) {
            return false;
        }
        grid.innerHTML = await response.text();

        for (const item of pager.querySelectorAll('.page-item')) {
            item.classList.toggle('active', item.dataset.page === String(page));
        }
        return true;
    } catch (error) {
        return false;
    }
}

pager.addEventListener('click', async event => {
    const link = event.target.closest('a[data-page]');
    if (link === null) {
        return;
    }
    event.preventDefault();
    const shown = await showPage(link.dataset.page);
    if (!shown) {
        window.location.href = link.href;
    }
});

How it works

One listener for the whole pager. Rather than a listener on every link, there's one on the pager. A click on a link bubbles up to its parents, so the pager hears it, and event.target.closest('a[data-page]') finds the link that was clicked (or null, for a click between links). Listening on a parent like this is called event delegation; it works however many links there are.

event.preventDefault() stops the browser following the link, so the page stays put.

showPage does the fetching:

  • fetch('/products/grid?page=2') asks the new action for the partial.
  • response.text() reads the body as text, the HTML, where an API's response.json() read JSON.
  • grid.innerHTML = ... replaces everything inside #product-grid with the new cards. innerHTML is right here because the HTML comes from your own server's Razor, which already encoded every product name safely.
  • classList.toggle('active', condition) adds the class when the condition is true and removes it when it's false, so exactly one page is marked active.

If the fetch doesn't work, whether the server answers with an error (response.ok is false) or doesn't answer at all (the catch), showPage returns false, and the script falls back to following the link the ordinary way. The user still gets their page.

The result

Click page 2: the cards change, the pager marks 2 as active, and nothing else on the page moves. The navigation bar, the heading and the Recently viewed line never reload. Press F12 and open the Network tab while you click: each click is one small request for grid?page=2, a fraction of the size of a whole page.

(The address bar still says /products. Making Back and bookmarks remember the page is possible with the browser's History API, but that's a topic for another day.)

Check yourself

2 questions to try on the lesson page.

So what

A partial view, an action that returns it, and a dozen lines of JavaScript turn a page that reloads into one that updates in place. And because the links still work on their own, nothing breaks when the JavaScript doesn't run.

Key terms

Cheat sheets