5

I've been working through the Symfony 3 tutorial on embedding a collection of forms, and I want to extend the idea to extra nested levels. I had a look around, and there are partial answers for Symfony 2, but nothing comprehensive (and nothing for 3).

If we take the tutorials Task has many Tag example, how would I code it so it extends to: Task has many Tag has many SubTag?

So far I think I understand the Form classes:

Task:

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('description');

        $builder->add('tags', CollectionType::class, array(
            'entry_type' => TagType::class,
            'allow_add' => true,
            'by_reference' => false,
            'allow_delete' => true
        ));
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Task',
        ));
    }
}

Tag:

class TagType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder->add('name');

            $builder->add('sub_tags', CollectionType::class, array(
                'entry_type' => SubTagType::class,
                'allow_add' => true,
                'by_reference' => false,
                'allow_delete' => true
            ));
        }

        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => 'AppBundle\Entity\Tag',
            ));
        }
    }

SubTag:

class SubTagType extends AbstractType
    {
        public function buildForm(FormBuilderInterface $builder, array $options)
        {
            $builder->add('name');
        }

        public function configureOptions(OptionsResolver $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => 'AppBundle\Entity\SubTag',
            ));
        }
    }

And the basic Twig class:

{{ form_start(form) }}
    {# render the task's only field: description #}
    {{ form_row(form.description) }}

    <h3>Tags</h3>
    <ul class="tags">
        {# iterate over each existing tag and render its only field: name #}
        {% for tag in form.tags %}
            <li>{{ form_row(tag.name) }}</li>
            {% for sub_tag in tag.sub_tags %}
                <li>{{ form_row(sub_tag.name) }}</li>
            {% endfor %}
        {% endfor %}
    </ul>
{{ form_end(form) }}

But it's at this point I'm unsure of how the prototype and javascript will work. Could somebody explain how I'd take this next step? Is this even the right approach?

My first thought is that if we're doing additional levels, it might be smart to generalize the JS for any number of levels, since the tutorial uses very JS that can only work on a single level.

The closest working code I can find is this stack overflow answer here. However, it doesn't appear to work as described, and Im having trouble working out exactly what's wrong.

2
  • Hey - let me know if you've run into a specific issue that I wasn't able to address in my answer. (I saw you had it marked "accepted" at one point.) Commented Oct 21, 2016 at 18:05
  • It unaccepted for some reason! Commented Oct 22, 2016 at 23:08

1 Answer 1

3

It's not any different than a regular embedded collection of forms.

However, if you want to avoid trouble with the default __NAME__ prototype colliding with a parent form's prototype string, you should take take to choose distinct values for the TagType and SubTag types.

From the Symfony Docs entry on CollectionType:

prototype_name

  • type: string default: name
  • If you have several collections in your form, or worse, nested collections you may want to change the placeholder so that unrelated placeholders are not replaced with the same value.

This can be very helpful if you want to abstract your clone actions with the javascript, like those in this article (pasted below), which - by the way - appears to target symfony3!

You might, for instance want to include the same value you pass to prototype_name, as an attr on the collection holder's html, so that you can access it dynamically, when doing the replace on the data-prototype html.

var $collectionHolder;

// setup an "add a tag" link
var $addTagLink = $('<a href="#" class="add_tag_link">Add a tag</a>');
var $newLinkLi = $('<li></li>').append($addTagLink);

jQuery(document).ready(function() {
// Get the ul that holds the collection of tags
$collectionHolder = $('ul.tags');

// add the "add a tag" anchor and li to the tags ul
$collectionHolder.append($newLinkLi);

// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find(':input').length);

$addTagLink.on('click', function(e) {
    // prevent the link from creating a "#" on the URL
    e.preventDefault();

    // add a new tag form (see next code block)
    addTagForm($collectionHolder, $newLinkLi);
});

function addTagForm($collectionHolder, $newLinkLi) {
    // Get the data-prototype explained earlier
    var prototype = $collectionHolder.data('prototype');

    // get the new index
    var index = $collectionHolder.data('index');

    // Replace '__name__' in the prototype's HTML to
    // instead be a number based on how many items we have
    var newForm = prototype.replace(/__name__/g, index);

    // increase the index with one for the next item
    $collectionHolder.data('index', index + 1);

    // Display the form in the page in an li, before the "Add a tag" link li
    var $newFormLi = $('<li></li>').append(newForm);
    $newLinkLi.before($newFormLi);
}
Sign up to request clarification or add additional context in comments.

4 Comments

Hah - anything I can do to win your favour back?
@Cameron, are you still there? I am facing exactly the issue you are addressing. I am not sure how you suggest to write the JS code. How do I write the 'add a subtag' part? thanks
Hey, @mario - I'm still here. Happy to help if you can narrow the question down to something more specific!
Hey @Cameron, thanks to reply. In the example, how do you write the JS to insert/clone a form for subtags? I am able to code a form for multiple Tags, and a form for multiple Subtags; I am not to code a unique form where you may create more Tags, and within each more Subtags. My understanding is that you need to mix up your code, but I am not sure how. (I am looking at this for a more couple of hours and then sometimes later than Thursday)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.