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.
Buying from5 to10Mug(s) will grant a5% discount.
Buying from11 to25Mug(s) will grant a10% discount.
Buying from26 to99Mug(s) will grant a20% 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> </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>> 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 %}