Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions assets/js/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,13 @@ export default class Autocomplete {
labelField: 'entityAsString',
searchField: ['entityAsString'],
firstUrl: (query) => {
return `${autocompleteEndpointUrl}&query=${encodeURIComponent(query)}`;
const url = new URL(autocompleteEndpointUrl);
url.searchParams.append(`query`, query);
this.#resolveAutocompleteDependencyFields(element).forEach((dependency) => {
url.searchParams.append(`autocompleteDependsOn[${dependency.path}]`, dependency.element.value);
});

return url.toString();
},
// VERY IMPORTANT: use 'function (query, callback) { ... }' instead of the
// '(query, callback) => { ... }' syntax because, otherwise,
Expand Down Expand Up @@ -145,7 +151,11 @@ export default class Autocomplete {
},
});

return this.#initializeTomSelect(element, config);
const tomSelect = this.#initializeTomSelect(element, config);

this.#bindAutocompleteDependencyFieldListeners(tomSelect);

return tomSelect;
}

#initializeTomSelect(element, config) {
Expand Down Expand Up @@ -258,4 +268,41 @@ export default class Autocomplete {

return { options, optgroups };
}

#bindAutocompleteDependencyFieldListeners(tomSelect) {
this.#resolveAutocompleteDependencyFields(tomSelect.input).forEach((dependency) => {
dependency.element.addEventListener('change', () => {
tomSelect.clear();
tomSelect.clearOptions();
tomSelect.clearOptionGroups();
tomSelect.clearPagination();
tomSelect.wrapper.classList.remove('preloaded');
});
});
}

#resolveAutocompleteDependencyFields(element) {
const dependsOn = JSON.parse(element.getAttribute('data-ea-autocomplete-depends-on') || '[]');
const form = element.closest('form');

if (null === form) {
return [];
}

return dependsOn.map((field) => {
const fieldId = `${form.name}_${field.replaceAll('.', '_')}`;
const fieldElement = document.getElementById(fieldId) ?? document.getElementById(`${fieldId}_autocomplete`);

if (null === fieldElement) {
console.error(`No field found for autocomplete dependency "${field}".`);
return null;
}

return {
path: field,
element: fieldElement,
};
})
.filter((dependencyField) => null !== dependencyField);
}
}
3 changes: 2 additions & 1 deletion src/Controller/AbstractCrudController.php
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,8 @@ public function autocomplete(AdminContext $context): JsonResponse
$queryBuilderCallable = $field?->getCustomOption(AssociationField::OPTION_QUERY_BUILDER_CALLABLE);

if (null !== $queryBuilderCallable) {
$queryBuilder = $queryBuilderCallable($queryBuilder) ?? $queryBuilder;
$autocompleteDependsOn = $context->getRequest()->query->all('autocompleteDependsOn');
$queryBuilder = $queryBuilderCallable($queryBuilder, $autocompleteDependsOn) ?? $queryBuilder;
}

$callback = $field?->getCustomOption(AssociationField::OPTION_AUTOCOMPLETE_CALLBACK)
Expand Down
11 changes: 10 additions & 1 deletion src/Field/AssociationField.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ final class AssociationField implements FieldInterface
public const OPTION_AUTOCOMPLETE = 'autocomplete';
public const OPTION_AUTOCOMPLETE_CALLBACK = 'autocompleteCallback';
public const OPTION_AUTOCOMPLETE_TEMPLATE = 'autocompleteTemplate';
public const OPTION_AUTOCOMPLETE_DEPENDS_ON = 'autocompleteDependsOn';
public const OPTION_EMBEDDED_CRUD_FORM_CONTROLLER = 'crudControllerFqcn';
public const OPTION_WIDGET = 'widget';
public const OPTION_QUERY_BUILDER_CALLABLE = 'queryBuilderCallable';
Expand Down Expand Up @@ -52,6 +53,7 @@ public static function new(string $propertyName, TranslatableInterface|string|bo
->setCustomOption(self::OPTION_AUTOCOMPLETE, false)
->setCustomOption(self::OPTION_AUTOCOMPLETE_CALLBACK, null)
->setCustomOption(self::OPTION_AUTOCOMPLETE_TEMPLATE, null)
->setCustomOption(self::OPTION_AUTOCOMPLETE_DEPENDS_ON, null)
->setCustomOption(self::OPTION_EMBEDDED_CRUD_FORM_CONTROLLER, null)
->setCustomOption(self::OPTION_WIDGET, self::WIDGET_AUTOCOMPLETE)
->setCustomOption(self::OPTION_QUERY_BUILDER_CALLABLE, null)
Expand All @@ -64,7 +66,10 @@ public static function new(string $propertyName, TranslatableInterface|string|bo
->setCustomOption(self::OPTION_PREFERRED_CHOICES, null);
}

public function autocomplete(bool $enable = true, ?callable $callback = null, ?string $template = null, bool $renderAsHtml = false): self
/**
* @param list<string>|string|null $dependsOn
*/
public function autocomplete(bool $enable = true, ?callable $callback = null, ?string $template = null, bool $renderAsHtml = false, array|string|null $dependsOn = null): self
{
if (!$enable) {
return $this;
Expand All @@ -83,6 +88,10 @@ public function autocomplete(bool $enable = true, ?callable $callback = null, ?s
// the renderAsHtml parameter controls the same option as renderAsHtml() method
$this->setCustomOption(self::OPTION_ESCAPE_HTML_CONTENTS, !$renderAsHtml);

if (null !== $dependsOn) {
$this->setCustomOption(self::OPTION_AUTOCOMPLETE_DEPENDS_ON, \is_string($dependsOn) ? [$dependsOn] : $dependsOn);
}

return $this;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Field/Configurator/AssociationConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
} elseif (null !== $autocompleteTemplate) {
$field->setFormTypeOption('autocomplete_template', $autocompleteTemplate);
}

$autocompleteDependsOn = $field->getCustomOption(AssociationField::OPTION_AUTOCOMPLETE_DEPENDS_ON);
if (null !== $autocompleteDependsOn) {
$field->setFormTypeOption('attr.data-ea-autocomplete-depends-on', json_encode($autocompleteDependsOn));
}
} else {
$field->setFormTypeOptionIfNotSet('query_builder', static function (EntityRepository $repository) use ($field) {
// TODO: should this use `createIndexQueryBuilder` instead, so we get the default ordering etc.?
Expand Down
Loading