12

I try to find the best way to render HTML through AJAX in Magento 2.

Way 1 : Using Controller without layout

File Foo/Bar/Controller/Popin/Content.php

<?php

namespace Foo\Bar\Controller\Popin;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;

/**
 * Class Content
 */
class Content extends Action
{

    /**
     * Content constructor.
     *
     * @param Context $context
     */
    public function __construct(
        Context $context
    ) {
        parent::__construct($context);
    }

    /**
     *
     */
    public function execute()
    {
        /** @var \Magento\Framework\View\Layout $layout */
        $layout = $this->_view->getLayout();

        /** @var \Foo\Bar\Block\Popin\Content $block */
        $block = $layout->createBlock(\Foo\Bar\Block\Popin\Content::class);
        $block->setTemplate('Foo_Bar::popin/content.phtml');

        $this->getResponse()->setBody($block->toHtml());
    }
}   

Way 2 : Using Controller with custom layout

File Foo/Bar/Controller/Popin/Content.php

<?php

namespace Foo\Bar\Controller\Popin;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;

/**
 * Class Content
 */
class Content extends Action
{

    /**
     * Content constructor.
     *
     * @param Context $context
     */
    public function __construct(
        Context $context
    ) {
        parent::__construct($context);
    }

    /**
     *
     */
    public function execute()
    {
        $this->_view->loadLayout();
        $this->_view->renderLayout();
    }
}    

File Foo/Bar/view/frontend/page_layout/ajax-empty.xml

<?xml version="1.0"?>

<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd">
    <container name="root"/>
</layout>

File Foo/Bar/view/frontend/layout/foo_bar_popin_content.xml

<?xml version="1.0"?>

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="ajax-empty" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="root">
            <block class="Foo\Bar\Block\Popin\Content" name="foo_bar_popin_content" template="Foo_Bar::popin/content.phtml" cacheable="false"/>
        </referenceContainer>
    </body>
</page>

IMO the best practice seems to be the Way 2 because it separate the logic from the Controller.
But the issue with the Way 2 is that the <body> and <head> with CSS / JS are generated so it is not a full cleaned HTML with only my block template in it.

  • am I using the custom layout the wrong way ?
  • Is the Way 1 is considered as a good practice ?
  • Are there any others ways to do that ?
17

I would also go the way 2 and, indeed, you actually can render "pure" HTML via AJAX without the head, body, css and so on.

The trick is to:

  • tell your controller to instanciate a Response that is of type \Magento\Framework\View\Result\Layout rather than \Magento\Framework\View\Result\Page
  • use a layout XML file with a root node that is <layout...>...</layout> rather than <page...>...</page>

Here is a very simple implementation.

The controller

<?php    
namespace Namespace\Module\Controller\Index;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Controller\ResultFactory;

class Index extends Action
{
    /**
     * Dispatch request
     *
     * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
     * @throws \Magento\Framework\Exception\NotFoundException
     */
    public function execute()
    {
        return $this->resultFactory->create(ResultFactory::TYPE_LAYOUT);
    }
}

The layout

<?xml version="1.0"?>
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd">
    <container name="root">
        <block class="Namespace\Module\Block\Some\Block" name="namespace_module.some_block" />
    </container>
</layout>

Example on Github

See this example module: https://github.com/herveguetin/Herve_AjaxLayout_M2

This module generates this:

enter image description here

  • What If I want load whole layout (XML with few containers, block etc)? create -> toHtml, and send via json to ajax? – mattkrupnik Aug 2 '17 at 12:07
5

Out of the box, Magento does not use any of those methods to render HTML via AJAX.

From what I've seen, everytime such thing needs to be done, JSON is used to transport the result.

Example from the Magento/Checkout/Controller/Cart/Add :

$this->getResponse()->representJson(
    $this->_objectManager->get('Magento\Framework\Json\Helper\Data')->jsonEncode($result)
);

Then Magento 2 uses a new mechanism called sections, to handle the data on the frontend and update the specific blocks that need to be updated, you can learn more about sections in this Q&A: https://magento.stackexchange.com/a/143381/2380

EDIT regarding the second part of my answer: as stated by Max in the comment, sections are only used with customer specific data and using this functionality instead of every AJAX call is not the right solution.

  • Yes I also use JSON to transport the result but I simplify my classes for the purpose of the question ;) But I am not aware of that section feature, it seems to be the right way to do what I want. I will take a look at it. I'll wait if there is any other response otherwise I'll validate your answer. Thanks ! – Matthéo Geoffray Jan 24 '17 at 14:25
  • 2
    I agree with using Json response instead of raw html data. But second part of your answer is not full correct. Note, that customer sections using only for customer-specific data and using this functionality instead of every Ajax call is not good idea. – Max Jan 24 '17 at 14:29
  • 2
    @Matthéo yes, i understood :) My comment addressed to Raphael for correcting answer, because second part of answer can be misunderstood by other users. – Max Jan 24 '17 at 14:49
  • 1
    @MaxStsepantsevich thanks for spotting that, I've edited my answer to reflect what you've said – Raphael at Digital Pianism Jan 24 '17 at 14:51
  • 1
    I added an answer using your feedbacks. Thanks you two for your help. – Matthéo Geoffray Jan 24 '17 at 14:55
3

In my example I can not use sections because it is not customer data and it is not after a PUT / POST action but using Raphael at Digital Pianism answer I figured out how Magento render sections.

If we take the example of cart section it use the method \Magento\Customer\CustomerData\SectionPool::getSectionDataByNames to retrieve data from sections. This lead us to \Magento\Checkout\CustomerData\Cart::getSectionData with a single array containing areas of the section, including $this->layout->createBlock('Magento\Catalog\Block\ShortcutButtons')->toHtml()

Depending on that here is the final Controller class:

<?php

namespace Foo\Bar\Controller\Popin;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Data\Form\FormKey\Validator;
use Psr\Log\LoggerInterface;

/**
 * Class Content
 */
class Content extends Action
{

    /**
     * @var LoggerInterface $logger
     */
    private $logger;
    /**
     * @var Validator $formKeyValidator
     */
    private $formKeyValidator;
    /**
     * @var JsonFactory $resultJsonFactory
     */
    private $resultJsonFactory;

    /**
     * Content constructor.
     *
     * @param Context $context
     * @param LoggerInterface $logger
     * @param Validator $formKeyValidator
     * @param JsonFactory $resultJsonFactory
     */
    public function __construct(
        Context $context,
        LoggerInterface $logger,
        Validator $formKeyValidator,
        JsonFactory $resultJsonFactory
    ) {
        $this->logger            = $logger;
        $this->formKeyValidator  = $formKeyValidator;
        $this->resultJsonFactory = $resultJsonFactory;
        parent::__construct($context);
    }

    /**
     *
     */
    public function execute()
    {
        if (!$this->formKeyValidator->validate($this->getRequest())) {
            return $this->resultRedirectFactory->create()->setPath('checkout/cart/');
        }

        /** @var \Magento\Framework\Controller\Result\Json $resultJson */
        $resultJson = $this->resultJsonFactory->create();

        try {
            /** @var \Magento\Framework\View\Layout $layout */
            $layout = $this->_view->getLayout();
            /** @var \Foo\Bar\Block\Popin\Content $block */
            $block = $layout->createBlock(\Foo\Bar\Block\Popin\Content::class);
            /** @var array $response */
            $response = [
                'content' => $block->toHtml(),
            ];
        } catch (\Exception $exception) {
            $resultJson->setStatusHeader(
                \Zend\Http\Response::STATUS_CODE_400,
                \Zend\Http\AbstractMessage::VERSION_11,
                'Bad Request'
            );
            /** @var array $response */
            $response = [
                'message' => __('An error occurred')
            ];
            $this->logger->critical($exception);
        }

        return $resultJson->setData($response);
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.