adt/ajax-select

Nette 扩展,用于 AJAX 选择控件。

v3.1.3 2024-04-19 13:27 UTC

README

安装

  1. 通过 composer 安装

    composer require adt/ajax-select
  2. 在您的 config.neon 中注册此扩展

    extensions:
        - ADT\Components\AjaxSelect\DI\AjaxSelectExtension
  3. 在您的 BasePresenter 中包含 AjaxServiceSignalTrait

    class BasePresenter extends ... {
        use \ADT\Components\AjaxSelect\Traits\AjaxServiceSignalTrait;
  4. assets/ajax-select.js 包含到您的前端构建中。

    <script type="text/javascript" src="vendor/ajax-select.min.js"></script>
  5. 创建您的第一个 AjaxEntity。

  6. 将此实体用于您的第一个 AjaxSelect 控件。

  7. 完成。

它是做什么的?

此扩展向 Nette\Forms\Container 和因此向所有派生类添加以下方法

  • addDynamicSelect($name, $title, $items, $itemFactory = null, $config = [])
    • 单值动态选择
    • $itemFactory: function (array $invalidValues, DynamicSelect $input)
  • addDynamicMultiSelect($name, $title, $items, $itemFactory = null, $config = [])
    • 多值动态选择
    • $itemFactory: 请参阅 addDynamicSelect
  • addAjaxSelect($name, $title, $entityName = $name, $entitySetupCallback = NULL, $config = [])
    • 单值 AJAX 选择
  • addAjaxMultiSelect($name, $title, $entityName = $name, $entitySetupCallback = NULL, $config = [])
    • 多值 AJAX 选择

配置

[
    AjaxSelectExtension::CONFIG_INVALID_VALUE_MODE => AjaxSelectExtension::INVALID_VALUE_MODE_*,
    AjaxSelectExtension::CONFIG_OR_BY_ID_FILTER => TRUE,
    AjaxSelectExtension::CONFIG_TRANSLATOR => TRUE,
]

AjaxSelectExtension::CONFIG_TRANSLATOR: 设置选择值的自动翻译开启/关闭。默认为 TRUE

动态选择

此控件允许将未知值传递到 $control->value 字段。这样做将调用控件的 $itemFactory 仅带一个参数 - 无效值。

项目工厂可以返回给定值的标题或空值(NULL,空字符串,零等)。非空值将自动追加到已知的有效值列表中。

DynamicSelect 接受数组或 \Kdyby\Doctrine\QueryObject,它扩展了 \ADT\BaseQuery\BaseQuery 中的 $items

如果传递了 QO,则实现以下功能

  • 函数 \ADT\BaseQuery\BaseQuery::callSelectPairsAuto() 定义是否应自动调用 \ADT\BaseQuery\BaseQuery::selectPairs() 函数。 selectPairs 将实体属性设置为选择键和值。
    • \ADT\BaseQuery\BaseQuery 中的默认值是 SELECT_PAIRS_KEY = 'id' 和 SELECT_PAIRS_VALUE = null,它返回整个对象,因此您应该根据需要覆盖值常量,例如到 name。在调用 selectPairs 函数或覆盖常量时,您还可以使用实体获取器名称,它返回更复杂的值。例如 nameWithEmail,然后从实体对象调用函数 getNameWithEmail
    • 当在 QO 中定义自定义获取函数时,应扩展 callSelectPairsAuto 函数,并且我们继续从 \ADT\BaseQuery\BaseQuery 的默认获取函数调用中获取数据。请参阅下面 DynamicSelect 中的函数示例。
  • \Kdyby\DoctrineForms\EntityForm 中的实体可以设置不活动的默认值,这会导致选中项中不允许错误。因此,调用 \ADT\BaseQuery\BaseQuery::orById() 函数,将该不活动值设置为项。
    • AjaxSelectExtension::CONFIG_OR_BY_ID_FILTER 可以关闭此默认调用。
    • 对于未由处理实体映射的属性,必须关闭此函数。例如,如果我们处于 UserForm 中。实体 User 有属性 idnamerole。在 UserForm 中,我们可以为 role 属性创建动态选择,其中 orByIdFilter 已开启。但如果我们为自定义项如 profession 创建动态选择,或ByIdFilter 必须关闭,因为它是 User 实体的非映射属性。

Ajax 选择

此控件需要我们称之为AjaxEntity的东西及其工厂。所有用户AjaxEntity都必须从我们的AbstractEntityAggregateEntity派生。

  • 如果使用AbstractEntity,则必须实现以下功能
    • createQueryObject:返回特定实体创建的查询对象,该实体继承自\ADT\BaseQuery\BaseQuery
    • filterQueryObject:在此函数中,您将调用来自您的QO的所有过滤器函数。
    • formatValues:在此处,您从QO获取过滤后的数据,将其格式化为所需的格式。

此AjaxEntity封装了$itemFactory的行为,但它可以变得更强大。

AjaxSelect还使用orByIdFilter,请参阅Dynamic Select

配置

实现AjaxEntity

首先,创建一个新的类(例如,UserAjaxEntity),它从我们的\ADT\Components\AjaxSelect\Entities\AbstractEntity派生。

\ADT\Components\AjaxSelect\Entities\AbstractEntity需要实现一些函数。下面是示例。

此外,我们还需要其工厂,因此也要创建一个接口(例如,IUserAjaxEntityFactory)。

示例

namespace App\Model\Ajax;

interface IUserAjaxEntityFactory {
    /** @return UserAjaxEntity */
    function create();
}

class UserAjaxEntity extends \ADT\Components\AjaxSelect\Entities\AbstractEntity {

    const OPTION_OR_BY_ID = 'orById';
    const OPTION_BY_ID = 'byId';
    const OPTION_ACTIVE = 'active';
    
    /** @var \Kdyby\Doctrine\EntityManager */
    protected $em;
	
    /** @var \App\Queries\IUserFactory This object is defined below in DynamicSelect implementation */
    protected $userQueryFactory;

    public function __construct(\Kdyby\Doctrine\EntityManager $em, \App\Queries\IUserFactory $userQueryFactory) {
        $this->em = $em;
        $this->userQueryFactory = $userQueryFactory;
    }
    
    /**
     * @param int|int[] $id
     * @return $this
     */
    public function orById($id) {
        return $this->set(static::OPTION_OR_BY_ID, $id);
    }

    /**
     * @param int|int[] $id
     * @return $this
     */
    public function byId($id) {
        return $this->set(static::OPTION_BY_ID, $id);
    }
    
    public function active($bool) {
        // if $bool is TRUE, only active users are shown,
        // otherwise, only inactive are shown
        return $this->set(self::OPTION_ACTIVE, $bool);
    }
    
    /**
     * This function is required by \ADT\Components\AjaxSelect\Entities\AbstractEntity
     * @param array $values 
     * @return array
     */
    public function formatValues($value) {
        // TODO return array of userId => userName
    }
    
    /**
     * This function is required by \ADT\Components\AjaxSelect\Entities\AbstractEntity
     * @internal
     * @return Queries\User
     */
    protected function createQueryObject()
    {
        return $this->userQueryFactory->create();
    }

    /**
     * This function is required by \ADT\Components\AjaxSelect\Entities\AbstractEntity
     * @internal
     * @param Queries\User $query
     */
    protected function filterQueryObject(&$query) {
        if ($value = $this->get(static::OPTION_OR_BY_ID)) {
            $query->orById($value);
        }
        
        if ($value = $this->get(static::OPTION_BY_ID)) {
            $query->byId($value);
        }
        
        if ($value = $this->get(static::OPTION_ACTIVE)) {
            $query->byActive($value);
        }
    }

}

然后,在您的config.neonservices部分注册此实体及其工厂

services:
    -
        create: \App\Model\Ajax\UserAjaxEntity
        implement: \App\Model\Ajax\IUserAjaxEntityFactory
        tags: [ajax-select.entity-factory]

这告诉Nette自动实现您的实体的工厂,并将其标记为ajax-select.entity-factory。AjaxSelect现在知道您的实体了。

现在,您可以从Nette表单中的AjaxSelect控件直接使用您的AjaxEntity

$form->addAjaxSelect('user', 'Active users with default user', function (UserAjaxEntity $ajaxEntity) {
    $ajaxEntity
        ->active(TRUE);
})
    ->setRequired(TRUE);

$form->addAjaxSelect('inactiveUser', 'Inactive users without default user', 'user', function (UserAjaxEntity $ajaxEntity) {
    $ajaxEntity
        ->active(FALSE);
}, [
    AjaxSelectExtension::CONFIG_OR_BY_ID_FILTER => FALSE
]);

参数$entityName和/或$entitySetupCallback可以省略。如果您省略$entityName,则它等于控件名称(即第一个参数$name)。

最后,在表单附加到演示者之后,您必须调用finalizing ajaxSelect。例如,您可以在您的BaseForm::attached($presenter)中这样做。

/** @var \ADT\Components\AjaxSelect\Services\EntityPoolService $ajaxEntityPoolService */
$ajaxEntityPoolService->invokeDone();

AjaxEntity的名称、其选项和查询URL序列化为控件的data-ajax-select HTML属性。

使用QueryObject实现DynamicSelect

首先,创建一个新的类(例如,User),它继承自\ADT\BaseQuery\BaseQuery

此外,我们还需要其工厂,因此也要创建一个接口(例如,IUserFactory)。

示例

namespace App\Queries;

interface IUserFactory {
    /** @return User */
    function create();
}

class User extends \ADT\BaseQuery\BaseQuery {

    const OPTION_ACTIVE = 'active';
    
    protected $fetchWithDataEmail = FALSE;
    
    public function active() {
        $this->filter[static::OPTION_ACTIVE] = function (\Kdyby\Doctrine\QueryBuilder $qb) {
            $qb->andWhere('e.active = TRUE');
        };
        return $this;
    }
	
    public function disableActiveFilter() {
        unset($this->filter[static::OPTION_ACTIVE]);
        return $this;
    }
    
    protected function doCreateQuery(\Kdyby\Persistence\Queryable $repository) {
        $qb = parent::doCreateQuery($repository);
        $qb->addOrderBy('e.name');

        return $qb;
    }
    
    /**
     * @param Queryable|null $repository
     * @param int $hydrationMode
     * @return array|\Kdyby\Doctrine\ResultSet|mixed|object|\stdClass|null
     */
    public function fetch(?Queryable $repository = null, $hydrationMode = AbstractQuery::HYDRATE_OBJECT)
    {
        $fetch = parent::fetch($repository, $hydrationMode);

        if ($this->fetchWithDataEmail) {
            $array = [];
            foreach ($fetch as $person) {
                $array[$person->getId()] = \Nette\Utils\Html::el('option')
                    ->setAttribute('value', $person->getId())
                    ->setHtml($person->getName())
                    ->setAttribute('data-email', $person->getEmail());
            }

            $fetch = $array;
        }

        return $fetch;
    }

    /**
     * @return $this
     */
    public function fetchOptionsWithEmail()
    {
        $this->fetchWithDataEmail = TRUE;

        return $this;
    }

    /**
     * @return bool
     */
    public function callSelectPairsAuto()
    {
        return ! $this->fetchWithDataEmail && parent::callSelectPairsAuto();
    }

}

现在,您可以在Nette表单上创建DynamicSelect。

// Active users with default user
$entityForm->addDynamicSelect('user', 'Active users', $this->userQueryFactory->create()->active())
    ->setRequired(TRUE);

// All users without default user
$entityForm->addDynamicSelect('allUser', 'All users', $this->userQueryFactory->create(), NULL, [
    \ADT\Components\AjaxSelect\DI\AjaxSelectExtension::CONFIG_OR_BY_ID_FILTER => FALSE
]);

// Active users with email in label
$entityForm->addDynamicSelect('userWithEmail', 'All users', $this->userQueryFactory->create()->selectPairs('nameWithEmail'));

// Attribute that is not mapped so CONFIG_OR_BY_ID_FILTER must be turned off
$entityForm->addDynamicSelect('profession', 'Profession', $this->userQueryFactory->create(), NULL, [
    \ADT\Components\AjaxSelect\DI\AjaxSelectExtension::CONFIG_OR_BY_ID_FILTER => FALSE
]);

更改信号名称

如果您需要更改在查询URL中使用的信号,请按以下步骤操作

  1. 编辑您的config.neon

    ajaxSelect:
        getItemsSignalName: yourSignalName
  2. 重命名特质方法

    重写use AjaxServiceSignalTrait;如下

    use AjaxServiceSignalTrait {
        handleGetAjaxItems as handleYourSignalName;
    }

故障排除

动态表单容器(如addDynamic和toMany)

如果您创建了一个包含AjaxEntity的输入的新表单容器,并在调用$ajaxEntityPoolService->invokeDone();之后创建它(通常在表单初始化后调用),那么Ajax搜索将无法正常工作。

此类错误的示例

<?php // Form.php

public function init($form) {

    $form->addDynamic('address', function ($container) {
        $container->addAjaxSelect('city', 'City', function ($ajaxEntity) {
            $ajaxEntity
                ->byCountryCode('CZ');
        });
    });
    
    // No container exists right now
}
{* Form.latte *}

<div n:foreach="[0, 1, 2] as $addressIndex">
    {* When you access $form['address'][$addressIndex], then the container, "city" input and its AjaxEntity are created *}
    {label $form['address'][$addressIndex]['city'] /} {input $form['address'][$addressIndex]['city']}
</div>

正确解决方案

<?php // Form.php

public function init($form) {

    $form->addDynamic('address', function ($container) {
        $container->addAjaxSelect('city', 'City', function ($ajaxEntity) {
            $ajaxEntity
                ->byCountryCode('CZ');
        });
    });

    // This will create 3 new containers, its "city" input and AjaxEntity
    $form->setDefaults([
        'address' => [
            0 => [],
            1 => [],
            2 => [],
        ],
    ]);
}
{* Form.latte *}

{* We render only those containers, which already exists. *}
<div n:foreach="$form['address']->getContainers() as $container">
    {label $container['city'] /} {input $container['city']}
</div>

待办事项

orById过滤嵌套select

如果您在toMany或addDynamic内部有一个select,则必须设置AjaxSelectExtension::CONFIG_OR_BY_ID_FILTER => FALSE,这样库就不会尝试访问主实体中的按名称的属性,这会导致错误。可以实施嵌套select的orById过滤扩展,我们可以通过在select容器中确定select嵌套的位置(可能有多层嵌套)来实现,然后根据这个位置调用所有嵌套级别的元素,而不是$form->getEntity()->get{$attributeName}(),而是$form->getEntity()->get{$nestedElement}($elementIndex)->...->get{$attributeName}()