Skip to content

Commit b5467fa

Browse files
committed
[AssociationField] Add support for dependent / chained AssociativeField
1 parent 5c38282 commit b5467fa

4 files changed

Lines changed: 63 additions & 4 deletions

File tree

assets/js/autocomplete.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,13 @@ export default class Autocomplete {
110110
labelField: 'entityAsString',
111111
searchField: ['entityAsString'],
112112
firstUrl: (query) => {
113-
return `${autocompleteEndpointUrl}&query=${encodeURIComponent(query)}`;
113+
const url = new URL(autocompleteEndpointUrl);
114+
url.searchParams.append(`query`, query);
115+
this.#resolveAutocompleteDependencyFields(element).forEach((dependency) => {
116+
url.searchParams.append(`autocompleteDependsOn[${dependency.path}]`, dependency.element.value);
117+
});
118+
119+
return url.toString();
114120
},
115121
// VERY IMPORTANT: use 'function (query, callback) { ... }' instead of the
116122
// '(query, callback) => { ... }' syntax because, otherwise,
@@ -145,7 +151,11 @@ export default class Autocomplete {
145151
},
146152
});
147153

148-
return this.#initializeTomSelect(element, config);
154+
const tomSelect = this.#initializeTomSelect(element, config);
155+
156+
this.#bindAutocompleteDependencyFieldListeners(tomSelect);
157+
158+
return tomSelect;
149159
}
150160

151161
#initializeTomSelect(element, config) {
@@ -258,4 +268,41 @@ export default class Autocomplete {
258268

259269
return { options, optgroups };
260270
}
271+
272+
#bindAutocompleteDependencyFieldListeners(tomSelect) {
273+
this.#resolveAutocompleteDependencyFields(tomSelect.input).forEach((dependency) => {
274+
dependency.element.addEventListener('change', () => {
275+
tomSelect.clear();
276+
tomSelect.clearOptions();
277+
tomSelect.clearOptionGroups();
278+
tomSelect.clearPagination();
279+
tomSelect.wrapper.classList.remove('preloaded');
280+
});
281+
});
282+
}
283+
284+
#resolveAutocompleteDependencyFields(element) {
285+
const dependsOn = JSON.parse(element.getAttribute('data-ea-autocomplete-depends-on') || '[]');
286+
const form = element.closest('form');
287+
288+
if (null === form) {
289+
return [];
290+
}
291+
292+
return dependsOn.map((field) => {
293+
const fieldId = `${form.name}_${field.replaceAll('.', '_')}`;
294+
const fieldElement = document.getElementById(fieldId) ?? document.getElementById(`${fieldId}_autocomplete`);
295+
296+
if (null === fieldElement) {
297+
console.error(`No field found for autocomplete dependency "${field}".`);
298+
return null;
299+
}
300+
301+
return {
302+
path: field,
303+
element: fieldElement,
304+
};
305+
})
306+
.filter((dependencyField) => null !== dependencyField);
307+
}
261308
}

src/Controller/AbstractCrudController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,8 @@ public function autocomplete(AdminContext $context): JsonResponse
561561
$queryBuilderCallable = $field?->getCustomOption(AssociationField::OPTION_QUERY_BUILDER_CALLABLE);
562562

563563
if (null !== $queryBuilderCallable) {
564-
$queryBuilder = $queryBuilderCallable($queryBuilder) ?? $queryBuilder;
564+
$autocompleteDependsOn = $context->getRequest()->query->all('autocompleteDependsOn');
565+
$queryBuilder = $queryBuilderCallable($queryBuilder, $autocompleteDependsOn) ?? $queryBuilder;
565566
}
566567

567568
$callback = $field?->getCustomOption(AssociationField::OPTION_AUTOCOMPLETE_CALLBACK)

src/Field/AssociationField.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ final class AssociationField implements FieldInterface
1616
public const OPTION_AUTOCOMPLETE = 'autocomplete';
1717
public const OPTION_AUTOCOMPLETE_CALLBACK = 'autocompleteCallback';
1818
public const OPTION_AUTOCOMPLETE_TEMPLATE = 'autocompleteTemplate';
19+
public const OPTION_AUTOCOMPLETE_DEPENDS_ON = 'autocompleteDependsOn';
1920
public const OPTION_EMBEDDED_CRUD_FORM_CONTROLLER = 'crudControllerFqcn';
2021
public const OPTION_WIDGET = 'widget';
2122
public const OPTION_QUERY_BUILDER_CALLABLE = 'queryBuilderCallable';
@@ -52,6 +53,7 @@ public static function new(string $propertyName, TranslatableInterface|string|bo
5253
->setCustomOption(self::OPTION_AUTOCOMPLETE, false)
5354
->setCustomOption(self::OPTION_AUTOCOMPLETE_CALLBACK, null)
5455
->setCustomOption(self::OPTION_AUTOCOMPLETE_TEMPLATE, null)
56+
->setCustomOption(self::OPTION_AUTOCOMPLETE_DEPENDS_ON, null)
5557
->setCustomOption(self::OPTION_EMBEDDED_CRUD_FORM_CONTROLLER, null)
5658
->setCustomOption(self::OPTION_WIDGET, self::WIDGET_AUTOCOMPLETE)
5759
->setCustomOption(self::OPTION_QUERY_BUILDER_CALLABLE, null)
@@ -64,7 +66,7 @@ public static function new(string $propertyName, TranslatableInterface|string|bo
6466
->setCustomOption(self::OPTION_PREFERRED_CHOICES, null);
6567
}
6668

67-
public function autocomplete(bool $enable = true, ?callable $callback = null, ?string $template = null, bool $renderAsHtml = false): self
69+
public function autocomplete(bool $enable = true, ?callable $callback = null, ?string $template = null, bool $renderAsHtml = false, array|string|null $dependsOn = null): self
6870
{
6971
if (!$enable) {
7072
return $this;
@@ -83,6 +85,10 @@ public function autocomplete(bool $enable = true, ?callable $callback = null, ?s
8385
// the renderAsHtml parameter controls the same option as renderAsHtml() method
8486
$this->setCustomOption(self::OPTION_ESCAPE_HTML_CONTENTS, !$renderAsHtml);
8587

88+
if (null !== $dependsOn) {
89+
$this->setCustomOption(self::OPTION_AUTOCOMPLETE_DEPENDS_ON, \is_string($dependsOn) ? [$dependsOn] : $dependsOn);
90+
}
91+
8692
return $this;
8793
}
8894

src/Field/Configurator/AssociationConfigurator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
203203
} elseif (null !== $autocompleteTemplate) {
204204
$field->setFormTypeOption('autocomplete_template', $autocompleteTemplate);
205205
}
206+
207+
$autocompleteDependsOn = $field->getCustomOption(AssociationField::OPTION_AUTOCOMPLETE_DEPENDS_ON);
208+
if (null !== $autocompleteDependsOn) {
209+
$field->setFormTypeOption('attr.data-ea-autocomplete-depends-on', json_encode($autocompleteDependsOn));
210+
}
206211
} else {
207212
$field->setFormTypeOptionIfNotSet('query_builder', static function (EntityRepository $repository) use ($field) {
208213
// TODO: should this use `createIndexQueryBuilder` instead, so we get the default ordering etc.?

0 commit comments

Comments
 (0)