Loading modal contents with JavaScript
Sometimes it is required to open a modal and show contents loaded with fetch or Ajax.
There are many ways to achieve this, and there can be many variations to this requirement.
As the saying goes: the devil is in the details.
For example, should a loading animation be shown or not?
And if so, when?
- The visitor clicks > Open the modal > Show a loading animation > Show content when loaded
- The visitor clicks > Show a loading animation > Open the modal with content when loaded
Tutorial Scenario
This tutorial implements one example that you can then hopefully adjust as needed.
We will render a product slider as the modal contents.
Again, adjust as needed. This example is meant to get you started.
1. Create a Magento Module
Since our functionality is not limited to frontend side code that can live inside a theme, we need a Magento module.
We will use the example module name Hyva_Example
.
The steps to create a Magento module are out of scope of this tutorial.
2. Create endpoint to serve the modal content
We need some kind of API to serve the desired content. In this example, we will use a standard Magento controller action.
Custom API or standard?
Depending on your needs, you might be able to get away with an exiting API, like the Magento GraphQL or REST API.
If so, you don't need to build the endpoint, but you will need to adjust the JavaScript to load the content accordingly.
We need three things in our module:
A frontend route definition
Create the file etc/frontend/routes.xml
The action class
Create the file Controller/Modal/Content.php
<?php
declare(strict_types=1);
namespace Hyva\Example\Controller\Modal;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\PageFactory;
class Content implements HttpGetActionInterface
{
private PageFactory $pageFactory;
public function __construct(PageFactory $pageFactory)
{
$this->pageFactory = $pageFactory;
}
public function execute()
{
return $this->pageFactory->create();
}
}
The layout XML file
Create the file view/frontend/layout/hyva_example_modal_content.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<update handle="hyva_product_slider" />
<body>
<referenceContainer name="content">
<!-- Important! The wrapper ID is specified in the htmlId attribute: -->
<container name="slider-wrapper" htmlTag="div" htmlId="dom-element-id">
<block name="slider" template="Magento_Catalog::product/slider/product-slider.phtml">
<arguments>
<argument name="class" xsi:type="string" translate="true">class-slider</argument>
<argument name="category_ids" xsi:type="string">22</argument>
<argument name="include_child_category_products" xsi:type="boolean">true</argument>
<argument name="page_size" xsi:type="string">100</argument>
<argument name="hide_rating_summary" xsi:type="boolean">true</argument>
<argument name="hide_details" xsi:type="boolean">true</argument>
</arguments>
</block>
</container>
</referenceContainer>
</body>
</page>
Remember to enable the new module, and flush the Magento cache after making any changes.
3. Create the modal
The high level approach is:
- We render the modal content with a "Loading" placeholder. We give the placeholder a wrapper element with
id="dom-element-id"
. - When visitor opens the modal, we also fetch the content.
- When the content is available, we replace the temporary "Loading" placeholder content with the real stuff.
All of this happens in the a .phtml template of our module or theme.
<?php
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Hyva\Theme\Model\ViewModelRegistry $viewModels */
?>
<script>
function fetchModalContent() {
let isContentLoaded = false
return {
loadContent() {
if (isContentLoaded) return
window.fetch(`${BASE_URL}example/modal/content`)
.then(response => response.text())
.then(content => {
isContentLoaded = true
hyva.replaceDomElement('#dom-element-id', content)
})
.catch(console.error)
}
}
}
</script>
<div x-data="{...hyva.modal(), ...fetchModalContent()}">
<button type="button" class="text-center p-2 px-6 border border-secondary rounded"
aria-haspopup="dialog"
@click="show('the-modal', $event); loadContent()"
>
<?= $escaper->escapeHtml(__('Open')) ?>
</button>
<?= $modalViewModel->createModal()
->withDialogRefName("the-modal")
->withContent("<div id='dom-element-id' class='mt-2'>{$escaper->escapeHtml(__('Loading products...'))}</div>")
->withAriaLabelledby('the-label')
->addDialogClass('border', 'border-1');
?>
</div>
Things of note
- The
loadContent
method is merged into the Alpine component defined byhyva.modal()
. - The example content DOM ID
dom-element-id
is specified in three places:- In the initial modal content, to indicate the target element to receive the content.
- In the layout XML, on the modal content wrapper container, so it is present in the response content.
- As the first argument of the
hyva.replaceDomElement
call in theloadContent
method.
- The contents of the slider used for this example are declared in layout XML. See the slider documentation for more details.
- Instead of declaring the modal content using
withContent()
as done in the example above, a separate .phtml template could be used, too.
Thanks to Adam from DrinkSupermarket.com for contributing this tutorial!