I wanted to ask how I can achieve dynamic cascading children dependency using a structure similar to what is given in Symfony cookbook: dynamic_form_modification_using_form_events but with an additional field, which is being populated by data based on which option was chosen by the user. I managed to make it work with dependency similar to sports->position, but I can't manage to make it work with structure categories->categoryChildren->childCategoryChildren, where both categoryChildren and childCategoryChildren are dynamically created. When I try to make an event listener for categoryChildren, I'm getting an error:
The child with the name "categoryChildren" does not exist.
This is my form type:
/**
* Class AddAdType
* @package App\Ad\AdBundle\Form\Type
*/
class CreateAdType extends AbstractType
{
/**
* @param CategoryRepository $categoryRepository
*/
public function __construct(
CategoryRepository $categoryRepository,
)
{
$this->categoryRepository = $categoryRepository;
}
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('description')
->add('price')
->add('category', EntityType::class, [
'class' => Category::class,
'choices' => $this->categoryRepository->findBy(['parent' => null]),
'choice_label' => 'name',
'required' => true,
'placeholder' => 'Wybierz kategorie',
]);
$this->categoryChildrenListener($builder);
$this->childCategoryChildrenListener($builder);
}
/**
* @param FormBuilderInterface $builder
*/
public function categoryChildrenListener(FormBuilderInterface $builder)
{
$formModifier = function (FormInterface $form, Category $category = null) {
$categoryChildren = null === $category ? [] : $this->categoryRepository->findBy(['parent' => $category]);
$form->add('categoryChildren', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
'choices' => $categoryChildren,
'mapped' => false,
'attr' => [
'onChange' => "getNewVal(this);"
]
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getCategory());
}
);
$builder->get('category')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$ad = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $ad);
}
);
}
/**
* @param FormBuilderInterface $builder
*/
public function childCategoryChildrenListener(FormBuilderInterface $builder)
{
$formChildCategoryModifier = function (FormInterface $form, Category $categoryChild = null) {
$childCategoryChildren = null === $categoryChild ? [] : $this->categoryRepository->findBy(['parent' => $categoryChild]);
$form->add('childCategoryChildren', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
'choices' => $childCategoryChildren,
'mapped' => false
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formChildCategoryModifier) {
$data = $event->getData();
$formChildCategoryModifier($event->getForm(), $data->getCategory());
}
);
$builder->get('categoryChildren')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formChildCategoryModifier) {
$ad = $event->getForm()->getData();
$formChildCategoryModifier($event->getForm()->getParent(), $ad);
}
);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Ad::class,
'csrf_protection' => false
]);
}
}
And this is my entity:
/**
* Class Ad
* @package App\Ad\AdBundle\Entity\Ad
*
* @ORM\Entity
* @ORM\Table(name="ad")
*/
class Ad
{
//[...]
/**
* @ORM\ManyToOne(targetEntity="App\Ad\CategoryBundle\Entity\Category")
* @ORM\JoinColumn(name="category_id", nullable=false, referencedColumnName="id")
*/
private $category;
private $categoryChildren;
private $childCategoryChildren;
//[...]
/**
* @return Category|null
*/
public function getCategory(): ?Category
{
return $this->category;
}
/**
* @param Category|null $category
* @return $this
*/
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
/**
* @param Category|null $categoryChildren
*/
public function setCategoryChildren(?Category $categoryChildren): self
{
$this->categoryChildren = $categoryChildren;
return $this;
}
/**
* @return Category|null
*/
public function getCategoryChildren(): ?Category
{
return $this->categoryChildren;
}
/**
* @param Category|null $childCategoryChildren
*/
public function setChildCategoryChildren(?Category $childCategoryChildren): self
{
$this->childCategoryChildren = $childCategoryChildren;
return $this;
}
/**
* @return Category|null
*/
public function getChildCategoryChildren(): ?Category
{
return $this->childCategoryChildren;
}
}
This is my ajax for category select:
let $category = $('#create_ad_category');
let $childCategory = $('#create_ad_categoryChildren');
checkEmptySelect();
if ( $category.change() ) {
$category.change(function() {
let $form = $(this).closest('form');
let data = {};
data[$category.attr('name')] = $category.val();
$.ajax({
url : $form.attr('action'),
type: $form.attr('method'),
data : data,
complete: function(html) {
$('#create_ad_categoryChildren').replaceWith(
$(html.responseText).find('#create_ad_categoryChildren')
);
checkEmptySelect()
$('#create_ad_categoryChildren').prepend('<option value="" selected="selected"> '
'Select a subcategory of ' $category.find("option:selected").text() ' ...</option>');
}
});
});
}
function checkEmptySelect() {
if ( !$('#create_ad_categoryChildren').val() ) {
$('.subcategories').hide();
// $('.childCategories').hide();
} else {
$('.subcategories').show();
}
}
And this is my ajax for categoryChildren field:
function getNewVal(item) {
let $form = $(this).closest('form');
let data = {};
data[$category.attr('name')] = $category.val();
data[$childCategory.attr('name')] = item.value;
$.ajax({
url : $form.attr('action'),
type: $form.attr('method'),
data : data,
complete: function(html) {
$('#create_ad_childCategoryChildren').replaceWith(
$(html.responseText).find('#create_ad_childCategoryChildren')
);
checkEmptySelect()
$('#create_ad_childCategoryChildren').prepend('<option value="" selected="selected"> '
'Select a subcategory of ' $('#create_ad_categoryChildren').find("option:selected").text() ' ...</option>');
}
});
}
I'm using Symfony 6.0. Any help will be appreciated, cheers.
CodePudding user response:
Solved; I changed an Event from POST_SUBMIT to PRE_SUBMIT in class CreateAdType, and changed logic in listeners. Also, I had to change an Ajax type in function getNewVal() from $form.attr('method') to 'POST', since form was trying to call 'GET' instead of 'POST'. Solution below:
CreateAdType.php:
//[...]
/**
* @param FormBuilderInterface $builder
*/
public function categoryChildrenListener(FormBuilderInterface $builder)
{
$formModifier = function (FormInterface $form, Category $category = null) {
$categoryChildren = null === $category ? [] : $this->categoryRepository->findBy(['parent' => $category]);
$form->add('categoryChildren', EntityType::class, [
'class' => Category::class,
'label' => 'Podkategorie',
'choice_label' => 'name',
'choices' => $categoryChildren,
'mapped' => false,
'attr' => [
'onChange' => "getNewVal(this);"
]
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getCategory());
}
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$ad = $event->getData();
$categoryId = array_key_exists('category', $ad) ? $ad['category'] : null;
$category = $this->categoryRepository->find($categoryId);
$formModifier($event->getForm(), $category);
}
);
}
/**
* @param FormBuilderInterface $builder
*/
public function childCategoryChildrenListener(FormBuilderInterface $builder)
{
$formChildCategoryModifier = function (FormInterface $form, Category $categoryChild = null) {
$childCategoryChildren = null === $categoryChild ? [] : $this->categoryRepository->findBy(['parent' => $categoryChild]);
$form->add('childCategoryChildren', EntityType::class, [
'class' => Category::class,
'label' => 'Podkategorie podkategorii',
'choice_label' => 'name',
'choices' => $childCategoryChildren,
'mapped' => false
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formChildCategoryModifier) {
$data = $event->getData();
$formChildCategoryModifier($event->getForm(), $data->getCategoryChildren());
}
);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formChildCategoryModifier) {
$ad = $event->getData();
$childCategory = null;
if ( array_key_exists('categoryChildren', $ad) ) {
if ( $ad['categoryChildren'] !== '' ) {
$childCategoryId = $ad['categoryChildren'];
$childCategory = $this->categoryRepository->find($childCategoryId);
}
}
$formChildCategoryModifier($event->getForm(), $childCategory);
}
);
}
//[...]
Ajax call for categoryChildren field:
function getNewVal(item) {
let $form = $(this).closest('form');
let data = {};
data[$category.attr('name')] = $category.val();
data[$childCategory.attr('name')] = item.value;
$.ajax({
url : $form.attr('action'),
type: "POST",
data : data,
complete: function(html) {
$('#create_ad_childCategoryChildren').replaceWith(
$(html.responseText).find('#create_ad_childCategoryChildren')
);
checkChildCategoryChildrenValue()
$('#create_ad_childCategoryChildren').prepend('<option value="" selected="selected"> '
'Wybierz podkategorie kategorii ' $('#create_ad_categoryChildren').find("option:selected").text() ' ...</option>');
}
});
}
function checkChildCategoryChildrenValue() {
if ( !$('#create_ad_childCategoryChildren').val() ) {
$('.childCategories').hide();
} else {
$('.childCategories').show();
}
}
I managed to implement some of the ideas from this post: How to add an Event Listener to a dynamically added field using Symfony Forms Hope it helps someone to save time and stress :)