heimrichhannot/contao-ajax-bundle

保持contao的ajax请求井然有序,并轻松处理响应。

1.5.0 2024-03-28 09:11 UTC

This package is auto-updated.

Last update: 2024-08-28 10:21:00 UTC


README

Contao Ajax Bundle

默认情况下,contao中的ajax请求并未集中。由于不同类型的模块中ajax请求的处理方式不同,简单的 \Environment::get('isAjaxRequest') 并不足以将请求委派给相关模块/方法。本模块提供了一个全局配置,您可以在 $GLOBALS['AJAX'] 中将您的模块作为一个自定义组附加,并添加带有所需参数的操作,这些参数应与请求进行核对。

技术说明

以下部分将向您展示如何注册和触发自定义ajax操作。

1. 配置/设置

以下示例显示了 [heimrichhannot/contao-formhybrid] (https://github.com/heimrichhannot/contao-formhybrid) 的ajax配置。

config.php
/**
 * Ajax Actions
 */
$GLOBALS['AJAX'][\HeimrichHannot\FormHybrid\Form::FORMHYBRID_NAME] = array
(
	'actions' => array
	(
		'toggleSubpalette' => array
		(
			'arguments' => array('subId', 'subField', 'subLoad'),
			'optional'   => array('subLoad'),
		),
		'asyncFormSubmit'  => array
		(
			'arguments' => array(),
			'optional'   => array(),
		),
		'reload'  => array
		(
			'arguments' => array(),
			'optional'   => array(),
			'csrf_protection' => true, // cross-site request forgery (ajax token check)
		),
	),
);

如您所见,我们有一个名为 formhybrid 的组,将所有带有此 group 参数的ajax请求委派给formhybrid。然后有一些操作 toggleSubpaletteasyncFormSubmit 等。这些方法必须在委托的上下文中以相同的名称存在。您可以提供应在函数内部调用并作为参数添加到方法中的参数。如果参数是 optional,则请求在没有参数的情况下有效,否则所有 arguments 必须在请求中存在,才能使ajax请求有效。如果您想保护ajax请求不受跨站违规的影响,则将 csrf_protection => true 添加到您的配置中,并不要忘记在每次请求时更新ajax url!

2. 如何创建我的ajax操作的url?

我们在 HeimrichHannot\AjaxBundle\Manager\AjaxActionManager 中提供了一个简单的辅助方法,称为 generateUrl。以下示例显示了如何在 FormHybrid 中创建 toggleSubpalette url。

\Contao\System::getContainer()->get('huh.ajax.action')->generateUrl(Form::FORMHYBRID_NAME, 'toggleSubpalette')

如您所见,我们默认不添加参数。这是在 toggleSubpalette 的相关javascript代码中完成的。您必须在您的javascript代码中检查,以确保参数 subIdsubFieldsubLoad 作为您的$_POST参数在ajax请求中由您自己提供。

jquery.formhybrid.js

toggleSubpalette: function (el, id, field, url) {
    el.blur();
    var $el = $(el),
        $item = $('#' + id),
        $form = $el.closest('form'),
        checked = true;

    var $formData = $form.serializeArray();

    $formData.push(
        {name: 'FORM_SUBMIT', value: $form.attr('id')},
        {name: 'subId', value: id},
        {name: 'subField',value: field});

    if ($el.is(':checkbox') || $el.is(':radio')) {
        checked = $el.is(':checked');
    }

    if (checked === false) {

        $.ajax({
            type: 'post',
            url: url,
            dataType: 'json',
            data: $formData,
            success: function (response) {
                $item.remove();
            }
        });

        return;
    }

    $formData.push(
        {name: 'subLoad', value: 1}
    );

    $.ajax({
        type: 'post',
        url: url,
        dataType: 'json',
        data: $formData,
        success: function (response, textStatus, jqXHR) {
            $item.remove();
            // bootstrapped forms
            if ($el.closest('form').find('.' + field).length > 0) {
                // always try to attach subpalette after wrapper element from parent widget
                $el.closest('form').find('.' + field).eq(0).after(response.result.html);
            } else {
                $el.closest('#ctrl_' + field).after(response.result.html);
            }
        }
    });
}

3. 我的ajax操作是如何触发的?

为了给您最大的自由度,我们决定始终处于contao上下文中。因此,所有请求前提条件都在默认的contao请求周期中进行检查。

toggleSubpalette 示例中,我们在 DC_Hybrid::__construct() 中触发操作,并提供一个 new FormAjax($this) 作为响应上下文。

注意:不要在Contao模块的generate()中调用"runActiveAction",因为那太晚了。最好在__construct()中运行它。

DC_Hybrid.php

public function __construct($strTable = '', $varConfig = null, $intId = 0)
{
    ...
    
    \Contao\System::getContainer()->get('huh.ajax')->runActiveAction(Form::FORMHYBRID_NAME, 'toggleSubpalette', new FormAjax($this));
    
    ...
    
}

那么这里做了什么?

  1. 您的javascript代码请求ajax操作。
  2. ajax操作被委派到当前页面,其中我们的 DC_Hybrid 扩展模块可用。
  3. Ajax 控制器将请求与 $GLOBALS['AJAX'] 配置中给定的 toggleSubpalette 参数进行核对。
  4. 如果所有要求都满足,并且方法 toggleSubpalette 在给定的上下文 new FormAjax($this) 中可用,则使用给定的参数触发该方法。
  5. 现在您可以在 toggleSubpalette 中执行模块相关的操作
  6. 如果您想向ajax请求返回数据,则必须返回一个有效的 HeimrichHannot\AjaxBundle\Response\Response 对象。
  7. 返回的 HeimrichHannot\AjaxBundle\Response\Response 对象将被转换为 JSON 对象,请求将在这里结束。

响应对象

目前我们实现了三个响应对象。

  1. HeimrichHannot\AjaxBundle\Response\ResponseSuccess
  2. HeimrichHannot\AjaxBundle\Response\ResponseError
  3. HeimrichHannot\AjaxBundle\Response\ResponseRedirect

ResponseSuccess

这将返回一个包含 HTTP 状态码 HTTP/1.1 200 OK 的 JSON 对象给 AJAX 动作。

示例

客户端
$.ajax({
    type: 'post',
    url: url,
    dataType: 'json',
    data: $formData,
    success: function (response, textStatus, jqXHR) {
        $item.remove();
        // bootstrapped forms
        if ($el.closest('form').find('.' + field).length > 0) {
            // always try to attach subpalette after wrapper element from parent widget
            $el.closest('form').find('.' + field).eq(0).after(response.result.html);
        } else {
            $el.closest('#ctrl_' + field).after(response.result.html);
        }
    }
});
服务器端
/**
 * Toggle Subpalette
 * @param      $id
 * @param      $strField
 * @param bool $blnLoad
 *
 * @return ResponseError|ResponseSuccess
 */
function toggleSubpalette($id, $strField, $blnLoad = false)
{
    $varValue = \Contao\System::getContainer()->get('huh.request')->getPost($strField) ?: 0;
    
    if (!is_array($this->dca['palettes']['__selector__']) || !in_array($strField, $this->dca['palettes']['__selector__'])) {
        \Controller::log('Field "' . $strField . '" is not an allowed selector field (possible SQL injection attempt)', __METHOD__, TL_ERROR);
        
        return new ResponseError();
    }
    
    $arrData = $this->dca['fields'][$strField];
    
    if (!Validator::isValidOption($varValue, $arrData, $this->dc)) {
        \Controller::log('Field "' . $strField . '" value is not an allowed option (possible SQL injection attempt)', __METHOD__, TL_ERROR);
        
        return new ResponseError();
    }
    
    if (empty(FormHelper::getFieldOptions($arrData, $this->dc))) {
        $varValue = (intval($varValue) ? 1 : '');
    }
    
    $this->dc->activeRecord->{$strField} = $varValue;
    
    $objResponse = new ResponseSuccess();
    
    if ($blnLoad)
    {
        $objResponse->setResult(new ResponseData($this->dc->edit(false, $id)));
    }
    
    return $objResponse;
}

ResponseError

这将返回一个包含 HTTP 状态码 HTTP/1.1 400 Bad Request 的 JSON 对象给 AJAX 动作。

示例

客户端
 $.ajax({
    url: url ? url : $form.attr('action'),
    dataType: 'json',
    data: $formData,
    method: $form.attr('method'),
    error: function(jqXHR, textStatus, errorThrown){
        if (jqXHR.status == 400) {
            alert(jqXHR.responseJSON.message);
            return;
        }
    }
});
服务器端
$objResponse = new ResponseRedirect();
$objResponse->setUrl($strUrl);
return $objResponse;

ResponseRedirect

这将返回一个包含 HTTP 状态码 HTTP/1.1 301 Moved Permanently 的 JSON 对象给 AJAX 动作。重定向 URL 包含在 xhr 响应对象 result.data.url 中;

示例

客户端
 $.ajax({
    url: url ? url : $form.attr('action'),
    dataType: 'json',
    data: $formData,
    method: $form.attr('method'),
    error: function(jqXHR, textStatus, errorThrown){
        if (jqXHR.status == 301) {
            location.href = jqXHR.responseJSON.result.data.url;
            return;
        }
    }
});
服务器端
$objResponse = new ResponseRedirect();
$objResponse->setUrl($strUrl);
die(json_encode($objResponse));

单元测试

对于单元测试,在 $GLOBALS 中将变量 UNIT_TESTING 设置为 true

//bootstrap.php

define('UNIT_TESTING', true);

然后你可以通过捕获 HeimrichHannot\AjaxBundle\Exception\AjaxExitException 来捕获你的测试中的 AJAX 结果。

// MyTestClass.php

    /**
     * @test
     */
    public function myTest()
    {
        $objRequest = \Contao\System::getContainer()->get('huh.request')->create('http://localhost' . AjaxAction::generateUrl('myAjaxGroup', 'myAjaxAction'), 'post');

        $objRequest->headers->set('X-Requested-With', 'XMLHttpRequest'); // xhr request

        Request::set($objRequest);

		$objForm = new TestPostForm();

        try
        {
            $objForm->generate();
            // unreachable code: if no exception is thrown after form was created, something went wrong
            $this->expectException(\HeimrichHannot\Ajax\Exception\AjaxExitException::class);
        } catch (AjaxExitException $e)
        {
            $objJson = json_decode($e->getMessage());

            $this->assertTrue(strpos($objJson->result->html, 'id="my_css_id"') > 0); // check that id is present within response
        }
    }