Put your collection in a table

By default, Symfony put the collection on a div, and each item of the collection on nested divs. So this plugin considers > div as a default value to get collection elements. You can overwrite this selector by setting elements_selector option.

Please note that if you choose to use a table, you'll need to have a great understanding on how form themes work, because most of themes uses divs and add extra markup, thus you'll need to overwrite many blocks.


Mininal bought quantityMaximal bought quantityDiscount (in percent) 
%
%
%

Buying from 5 to 10 Mug(s) will grant a 5% discount.

Buying from 11 to 25 Mug(s) will grant a 10% discount.

Buying from 26 to 99 Mug(s) will grant a 20% discount.


Code used:

    <script type="text/javascript">

        $('.discount-collection').collection({
            allow_duplicate: true,
            allow_up: false,
            allow_down: false,
            add: '<a href="#" class="btn btn-default" title="Add element"><span class="glyphicon glyphicon-plus-sign"></span></a>',

            // here is the magic!
            elements_selector: 'tr.item',
            elements_parent_selector: '%id% tbody'
        });

    </script>

<?php

namespace Fuz\AppBundle\Controller\Basic;

use Fuz\AppBundle\Base\BaseController;
use Fuz\AppBundle\Entity\Basic\InATable\Discount;
use Fuz\AppBundle\Entity\Basic\InATable\DiscountCollection;
use Fuz\AppBundle\Form\Basic\InATable\DiscountCollectionType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/basic")
 */
class InATableController extends BaseController
{
    /**
     * Using a table instead of a div
     *
     * @Route("/inATable", name="inATable")
     * @Template()
     */
    public function indexAction(Request $request)
    {
        $discounts = new DiscountCollection();

        $discounts->setProductName('Mug');

        // 5% discount when buying from 5 to 10 items
        $discountA = new Discount();
        $discountA->setQuantityFrom(5);
        $discountA->setQuantityTo(10);
        $discountA->setDiscount(5);
        $discounts->getDiscounts()->add($discountA);

        // 10% discount when buying from 11 to 25 items
        $discountB = new Discount();
        $discountB->setQuantityFrom(11);
        $discountB->setQuantityTo(25);
        $discountB->setDiscount(10); // 10%
        $discounts->getDiscounts()->add($discountB);

        // 20% discount when buying from 26 to 99 items
        $discountC = new Discount();
        $discountC->setQuantityFrom(26);
        $discountC->setQuantityTo(99);
        $discountC->setDiscount(20); // 20%
        $discounts->getDiscounts()->add($discountC);

        $form = $this->createForm(DiscountCollectionType::class, $discounts);
        if ($request->isMethod('POST')) {
            $form->handleRequest($request);
        }

        return [
            'form' => $form->createView(),
            'data' => $discounts,
        ];
    }
}
<?php

namespace Fuz\AppBundle\Entity\Basic\InATable;

class Discount
{
    private $quantityFrom;
    private $quantityTo;
    private $discount;

    public function getQuantityFrom()
    {
        return $this->quantityFrom;
    }

    public function setQuantityFrom($quantityFrom)
    {
        $this->quantityFrom = $quantityFrom;

        return $this;
    }

    public function getQuantityTo()
    {
        return $this->quantityTo;
    }

    public function setQuantityTo($quantityTo)
    {
        $this->quantityTo = $quantityTo;

        return $this;
    }

    public function getDiscount()
    {
        return $this->discount;
    }

    public function setDiscount($discount)
    {
        $this->discount = $discount;

        return $this;
    }
}
<?php

namespace Fuz\AppBundle\Entity\Basic\InATable;

use Doctrine\Common\Collections\ArrayCollection;

class DiscountCollection
{
    protected $productName;
    protected $discounts;

    public function __construct()
    {
        $this->discounts = new ArrayCollection();
    }

    public function getProductName()
    {
        return $this->productName;
    }

    public function setProductName($productName)
    {
        $this->productName = $productName;

        return $this;
    }

    public function getDiscounts()
    {
        return $this->discounts;
    }
}
<?php

namespace Fuz\AppBundle\Form\Basic\InATable;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DiscountCollectionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('productName', TextType::class, [
                'label' => 'Product name',
            ])
           ->add('discounts', CollectionType::class, [
                'label'        => 'Manage product discounts based on quantity bought.',
                'entry_type'   => DiscountType::class,
                'entry_options' => [
                    'attr' => [
                        'class' => 'item', // we want to use 'tr.item' as collection elements' selector
                    ],
                ],
                'allow_add'    => true,
                'allow_delete' => true,
                'prototype'    => true,
                'required'     => false,
                'by_reference' => true,
                'delete_empty' => true,
                'attr' => [
                    'class' => 'table discount-collection',
                ],

            ])
            ->add('save', SubmitType::class, [
                'label' => 'Save',
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'Fuz\AppBundle\Entity\Basic\InATable\DiscountCollection',
        ]);
    }

    public function getBlockPrefix()
    {
        return 'DiscountCollectionType';
    }
}
<?php

namespace Fuz\AppBundle\Form\Basic\InATable;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\PercentType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class DiscountType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('quantityFrom', IntegerType::class, [
                'label' => false,
            ])
            ->add('quantityTo', IntegerType::class, [
                'label' => false,
            ])
            ->add('discount', PercentType::class, [
                'label' => false,
                'type' => 'integer',
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => 'Fuz\AppBundle\Entity\Basic\InATable\Discount',
        ]);
    }

    public function getBlockPrefix()
    {
        return 'DiscountType';
    }
}

{% block DiscountCollectionType_widget %}
    {{ form_row(form.productName) }}

    {# form_row would write extra markup, so we directly write the collection #}
    {{ form_widget(form.discounts) }}
{% endblock %}

{# By default, collection uses the form_widget block to create its markup, but we want a table #}
{% block collection_widget %}
    {% spaceless %}

    {#
     # This is almost a copy/paste of jquery.collection.html.twig, we can't use it as it also
     # use form_widget. Note that we also use form_widget(prototype) instead of form_row(prototype)
     # to avoid generating extra markup.
     #}
    {% if prototype is defined %}
        {% set attr = attr|merge({'data-prototype': form_widget(prototype)}) %}
        {% set attr = attr|merge({'data-prototype-name': prototype.vars.name}) %}
    {% endif %}
    {% set attr = attr|merge({'data-allow-add': allow_add ? 1 : 0}) %}
    {% set attr = attr|merge({'data-allow-remove': allow_delete ? 1 : 0 }) %}
    {% set attr = attr|merge({'data-name-prefix': full_name}) %}

    <fieldset class="well">
        <label>{{ form_label(form) }}</label>

        {{ form_errors(form) }}

        {# Don't forget to add the collection attributes in your markup #}
        <table {{ block('widget_attributes') }}>
            <thead>
                <th>Mininal bought quantity</th>
                <th>Maximal bought quantity</th>
                <th>Discount (in percent)</th>
                <th>&nbsp;</th>
            </thead>
            <tbody>

                {#
                 # we can't form_widget(form) as it would render parent markup for a collection, so
                 # we iterate manually on children
                 #}
                {% for item in form %}
                    {{ form_widget(item) }}
                {% endfor %}

            </tbody>
        </table>
    </fieldset>

    {% endspaceless %}
{% endblock %}

{% block DiscountType_widget %}

    {# widget_attributes will generate class="item" from the DiscountCollectionType.entry_options configuration #}
    <tr {{ block('widget_attributes') }}>
        <td>{{ form_widget(form.quantityFrom) }}</td>
        <td>{{ form_widget(form.quantityTo) }}</td>
        <td>{{ form_widget(form.discount) }}</td>
        <td class="text-center">
            <a href="#" class="collection-remove btn btn-default" title="Delete element"><span class="glyphicon glyphicon-trash"></span></a>
            <a href="#" class="collection-add btn btn-default" title="Add element"><span class="glyphicon glyphicon-plus-sign"></span></a>
            <a href="#" class="collection-duplicate btn btn-default" title="Duplicate element"><span class="glyphicon glyphicon-th-large"></span></a>
        </td>
    </tr>

{% endblock %}

{% block DiscountType_label %}{% endblock %}

{% extends 'FuzAppBundle::layout.html.twig' %}

{% block extra_js %}
    <script src="{{ asset('js/jquery.collection.js') }}"></script>
{% endblock %}

{% block title %}Put your collection in a table{% endblock %}

{% block body %}

    <h2>{{ block('title') }}</h2>

    <p>
        By default, Symfony put the collection on a <code>div</code>, and each item of the collection on nested divs.
        So this plugin considers <code>&gt; div</code> as a default value to get collection elements. You can overwrite
        this selector by setting <code>elements_selector</code> option.
    </p>

    <p>
        Please note that if you choose to use a table, you'll need to have a great understanding on how form themes
        work, because most of themes uses divs and add extra markup, thus you'll need to overwrite many blocks.
    </p>

    <hr/>

    {% form_theme form 'FuzAppBundle:Basic/InATable:form-theme.html.twig' %}

    {{ form(form) }}

    <hr/>

    {% for item in data.discounts %}

        <p>
            Buying from {{ item.quantityFrom }} to {{ item.quantityTo }} {{ data.productName }}(s)
            will grant a {{ item.discount }}% discount.
        </p>

    {% endfor %}

    <hr/>

    <p>Code used:</p>
    <pre>{{ block('script') | e }}</pre>

    {{
        tabs([
            'Controller/Basic/InATableController.php',
            'Entity/Basic/InATable/DiscountCollection.php',
            'Entity/Basic/InATable/Discount.php',
            'Form/Basic/InATable/DiscountCollectionType.php',
            'Form/Basic/InATable/DiscountType.php',
            'Resources/views/Basic/InATable/index.html.twig',
            'Resources/views/Basic/InATable/form-theme.html.twig',
        ])
    }}

{% endblock %}

{% block script %}

    <script type="text/javascript">

        $('.discount-collection').collection({
            allow_duplicate: true,
            allow_up: false,
            allow_down: false,
            add: '<a href="#" class="btn btn-default" title="Add element"><span class="glyphicon glyphicon-plus-sign"></span></a>',

            // here is the magic!
            elements_selector: 'tr.item',
            elements_parent_selector: '%id% tbody'
        });

    </script>

{% endblock %}