adt / ajax-select
Nette extension for AJAX select controls.
Installs: 31 757
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 14
Forks: 2
Open Issues: 4
Requires
- php: >=7.1
- adt/base-query: ^1.7
- nette/application: *
- nette/di: ^3.0
- nette/forms: ^3.0
- nette/utils: ^3.0 | ^4.0
- dev-master
- v3.1.3
- v3.1.2
- v3.1.1
- v3.1
- v3.0
- v2.2.2
- v2.2.1
- v2.2
- v2.1
- v2.0
- v1.6.2
- v1.6.1
- v1.6
- 1.5
- v1.4.1
- v1.4
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3
- v1.2.4
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2
- v1.1.2
- v1.1.1
- v1.1
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0
- dev-add-aria-setsize-and-aria-posinset-attr-to-results
- dev-fix-OrdByIdFilterInterface
- dev-nette-2.1
This package is auto-updated.
Last update: 2024-11-07 08:33:48 UTC
README
Installation
-
Install via composer:
composer require adt/ajax-select
-
Register this extension in your config.neon:
extensions: - ADT\Components\AjaxSelect\DI\AjaxSelectExtension
-
Include
AjaxServiceSignalTrait
in yourBasePresenter
:class BasePresenter extends ... { use \ADT\Components\AjaxSelect\Traits\AjaxServiceSignalTrait;
-
Include
assets/ajax-select.js
to your front-end build.<script type="text/javascript" src="vendor/ajax-select.min.js"></script>
-
Create your first AjaxEntity.
-
Use this entity in your first AjaxSelect control.
-
Done.
What does it do?
This extension adds following methods to Nette\Forms\Container
and thus to all derived classes:
addDynamicSelect($name, $title, $items, $itemFactory = null, $config = [])
- dynamic select with one value
$itemFactory
:function (array $invalidValues, DynamicSelect $input)
addDynamicMultiSelect($name, $title, $items, $itemFactory = null, $config = [])
- dynamic select with multiple values
$itemFactory
: seeaddDynamicSelect
addAjaxSelect($name, $title, $entityName = $name, $entitySetupCallback = NULL, $config = [])
- ajax select with one value
addAjaxMultiSelect($name, $title, $entityName = $name, $entitySetupCallback = NULL, $config = [])
- ajax select with multiple values
Config
[ AjaxSelectExtension::CONFIG_INVALID_VALUE_MODE => AjaxSelectExtension::INVALID_VALUE_MODE_*, AjaxSelectExtension::CONFIG_OR_BY_ID_FILTER => TRUE, AjaxSelectExtension::CONFIG_TRANSLATOR => TRUE, ]
AjaxSelectExtension::CONFIG_TRANSLATOR
: Sets the automatic translation of select values on/off. Default is TRUE
.
Dynamic Select
This control allows passing unknown value to $control->value
field. Doing so will invoke control's $itemFactory
with only one parameter - the invalid value.
The item factory can either return title for given value or empty value (NULL
, empty string, zero etc.). Non-empty value is automatically appended to known list of valid values.
DynamicSelect accepts array or \Kdyby\Doctrine\QueryObject
that extends \ADT\BaseQuery\BaseQuery
in $items
.
Following features are implemented if QO is passed:
- Function
\ADT\BaseQuery\BaseQuery::callSelectPairsAuto()
defines if\ADT\BaseQuery\BaseQuery::selectPairs()
function should be called automatically.selectPairs
sets entity attributes as select key and value.- Default values in
\ADT\BaseQuery\BaseQuery
areSELECT_PAIRS_KEY
= 'id' andSELECT_PAIRS_VALUE
= null, which returns whole object, so you should override the value constant for your needs, for example toname
. When callingselectPairs
function or overriding the constant, you can also use entity getter name which returns more complex value. For examplenameWithEmail
which then calls functiongetNameWithEmail
from entity object. - Function
callSelectPairsAuto
should be expanded when custom fetch function in QO is defined, and we continue to process fetched data from default call of fetch function in\ADT\BaseQuery\BaseQuery
. See the function in the DynamicSelect example below.
- Default values in
- Entity in
\Kdyby\DoctrineForms\EntityForm
can have inactive default value set, which causes error of not allowed in selected items. Therefore, the automatic call of\ADT\BaseQuery\BaseQuery::orById()
function is called, which sets this inactive value to items.AjaxSelectExtension::CONFIG_OR_BY_ID_FILTER
can turn off this default call.- This function must be turned off for attributes that are not mapped by processed entity. For example if we are in
UserForm
. EntityUser
has propertiesid
,name
androle
. InUserForm
we can create dynamicSelect with orByIdFilter turned on forrole
property. But if we create dynamicSelect for custom item likeprofession
, orByIdFilter must be turned off, because it's not mapped property ofUser
entity.
Ajax Select
This control needs something we call AjaxEntity, and its factory. All user AjaxEntities need to derive from our AbstractEntity
or AggregateEntity
.
- if
AbstractEntity
is used, you must implement following functions:createQueryObject
which returns created query object of specific entity that inherits from\ADT\BaseQuery\BaseQuery
.filterQueryObject
: in this function you call all filter functions from your QO.formatValues
: here you get the filtered data from your QO to format them to the desired form.
This AjaxEntity encapsulates $itemFactory
's behaviour but it can get much more powerful.
AjaxSelect also uses orByIdFilter, see Dynamic Select
.
Configuration
Implement AjaxEntity
First, create new class (ie. UserAjaxEntity
) that derives from our \ADT\Components\AjaxSelect\Entities\AbstractEntity
.
\ADT\Components\AjaxSelect\Entities\AbstractEntity
requires few functions to be implemented. See example below.
In addition, we will need its factory, so create an interface (ie. IUserAjaxEntityFactory
) too.
Example:
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); } } }
Then register this entity and its factory in your config.neon
in services
section:
services: - create: \App\Model\Ajax\UserAjaxEntity implement: \App\Model\Ajax\IUserAjaxEntityFactory tags: [ajax-select.entity-factory]
This instructs Nette to autoimplement a factory for your entity and tag it as ajax-select.entity-factory
. AjaxSelect knows about your entity now.
Now you can use your AjaxEntity direcly from your AjaxSelect control on your Nette form:
$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 ]);
Arguments $entityName
and/or $entitySetupCallback
can be omitted. You can omit $entityName
if it's equal to control name (ie. first argument $name
).
Finally you have to call finalizing ajaxSelect after the form is attached to presenter.
For example you can do it in your BaseForm::attached($presenter)
/** @var \ADT\Components\AjaxSelect\Services\EntityPoolService $ajaxEntityPoolService */ $ajaxEntityPoolService->invokeDone();
AjaxEntity name, its options and query URL are serialized to control's data-ajax-select
HTML attribute.
Implement DynamicSelect with QueryObject
First, create new class (ie. User
) that derives from \ADT\BaseQuery\BaseQuery
.
In addition, We will need its factory, so create an interface (ie. IUserFactory
) too.
Example:
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(); } }
Now you can create DynamicSelect on your Nette form:
// 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 ]);
Change signal name
If you ever need to change signal that is used in query URL, proceed as follows:
-
edit your
config.neon
ajaxSelect: getItemsSignalName: yourSignalName
-
rename trait method
Rewrite
use AjaxServiceSignalTrait;
as followsuse AjaxServiceSignalTrait { handleGetAjaxItems as handleYourSignalName; }
Troubleshooting
Dynamic form containers (like addDynamic and toMany)
If you create a new form container which contains an input with AjaxEntity and you create it after calling $ajaxEntityPoolService->invokeDone();
(which is called typically after form initialization), then the ajax search will not work properly.
Example of such mistake:
<?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>
Right solution:
<?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>
ToDo
orById filtr pro zanořené selecty
Pokud máme select uvnitř toMany nebo addDynamic, tak musí být nastaveno AjaxSelectExtension::CONFIG_OR_BY_ID_FILTER => FALSE
, aby se knihovna nepokoušela přistoupit k atributu dle názvu selectu v hlavní entitě, což by skončilo chybou. Toto rozšíření orById filtru pro zanořené selecty by se dalo naimplementovat, že bychom si v kontejneru selectu zjistili, kam je select zanořený (může být víc než jedna úroveň zanoření) a podle toho bychom místo $form->getEntity()->get{$atributeName}()
provolali všechny prvky zanoření, tedy $form->getEntity()->get{$zanořenýPrvek}($indexPrvku)->...->get{$atributeName}()