Advanced usage: custom theme and several fields

An example that shows how works theming on a form with more than one field.

Name
Company
Street
Postalcode
City
Country

<-+>

Mickael Steller atfuz.org,41 rue de la Paix,75002Paris,France

File: Base/BaseController.php
<?php

namespace Fuz\AppBundle\Base;

use Fuz\AppBundle\Entity\Value;
use Fuz\AppBundle\Form\ValueType;
use Fuz\QuickStartBundle\Base\BaseController as QuickStartBase;
use Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\HttpFoundation\Request;

class BaseController extends QuickStartBase
{
    protected function createContextSample(Request $request, $name = 'form', $values = ['a', 'b', 'c'])
    {
        $data = ['values' => $values];

        $form = $this
           ->get('form.factory')
           ->createNamedBuilder($name, Type\FormType::class, $data)
           ->add('values', Type\CollectionType::class, [
               'entry_type'    => Type\TextType::class,
               'label'         => 'Add, move, remove values and press Submit.',
               'entry_options' => [
                   'label' => 'Value',
               ],
               'allow_add'    => true,
               'allow_delete' => true,
               'prototype'    => true,
               'attr'         => [
                   'class' => "{$name}-collection",
               ],
           ])
           ->add('submit', Type\SubmitType::class)
           ->getForm()
        ;

        $form->handleRequest($request);
        if ($form->isValid()) {
            $data = $form->getData();
        }

        return [
            $name         => $form->createView(),
            "{$name}Data" => $data,
        ];
    }

    protected function createAdvancedContextSample(Request $request, $name = 'advancedForm')
    {
        $a = new Value('a');
        $b = new Value('b');
        $c = new Value('c');

        $data = ['values' => [$a, $b, $c]];

        $form = $this
           ->get('form.factory')
           ->createNamedBuilder($name, Type\FormType::class, $data)
           ->add('values', Type\CollectionType::class, [
               'entry_type'   => ValueType::class,
               'label'        => 'Add, move, remove values and press Submit.',
               'allow_add'    => true,
               'allow_delete' => true,
               'prototype'    => true,
               'required'     => false,
               'attr'         => [
                   'class' => "{$name}-collection",
               ],
           ])
           ->add('submit', Type\SubmitType::class)
           ->getForm()
        ;

        $form->handleRequest($request);
        if ($form->isValid()) {
            $data = $form->getData();
        }

        return [
            $name         => $form->createView(),
            "{$name}Data" => $data,
        ];
    }
}
File: Base/BaseController.php
File: Controller/AdvancedController.php
<?php

namespace Fuz\AppBundle\Controller;

use Fuz\AppBundle\Base\BaseController;
use Fuz\AppBundle\Entity\Address;
use Fuz\AppBundle\Entity\Addresses;
use Fuz\AppBundle\Entity\Fancy;
use Fuz\AppBundle\Entity\FancyCollection;
use Fuz\AppBundle\Entity\Value;
use Fuz\AppBundle\Form\AddressesType;
use Fuz\AppBundle\Form\FancyCollectionType;
use Fuz\AppBundle\Form\MyArrayType;
use Fuz\AppBundle\Form\ValueType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/advanced")
 */
class AdvancedController extends BaseController
{
    /**
     * Advanced usage
     *
     * You can reference your form collection in the view, instead of
     * putting a selector in the form type.
     *
     * @Route("/mvcCompliance", name="mvcCompliance")
     * @Template()
     */
    public function mvcComplianceAction(Request $request)
    {
        $data = ['values' => ['a', 'b', 'c']];

        $form = $this
           ->createFormBuilder($data)
           ->add('values', Type\CollectionType::class, [
               'entry_type'    => Type\TextType::class,
               'label'         => 'Add, move, remove values and press Submit.',
               'entry_options' => [
                   'label' => 'Value',
               ],
               'allow_add'    => true,
               'allow_delete' => true,
               'prototype'    => true,
               'required'     => false,
//                   'attr' => array (
//                           'class' => 'my-selector', <--- Not MVC compliant!
//                   ),
           ])
           ->add('submit', Type\SubmitType::class)
           ->getForm()
        ;

        $form->handleRequest($request);
        if ($form->isValid()) {
            $data = $form->getData();
        }

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

    /**
     * Advanced usage
     *
     * A custom form theme helps define button's layout and positions as and where you want.
     *
     * @Route("/customFormTheme", name="customFormTheme")
     * @Template()
     */
    public function customFormThemeAction(Request $request)
    {
        $data = ['values' => [new Value('a'), new Value('b'), new Value('c')]];

        $form = $this
           ->createFormBuilder($data)
           ->add('values', Type\CollectionType::class, [
               'entry_type'   => ValueType::class,
               'label'        => 'Add, move, remove values and press Submit.',
               'allow_add'    => true,
               'allow_delete' => true,
               'prototype'    => true,
               'required'     => false,
               'attr'         => [
                   'class' => 'collection',
               ],
           ])
           ->add('submit', Type\SubmitType::class)
           ->getForm()
        ;

        $form->handleRequest($request);
        if ($form->isValid()) {
            $data = $form->getData();
        }

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

    /**
     * Advanced usage
     *
     * Same demo as above, but with the add button located at the bottom of
     * the form instead of close to each field.
     *
     * @Route("/customFormThemeAddBottom", name="customFormThemeAddBottom")
     * @Template()
     */
    public function customFormThemeAddBottomAction(Request $request)
    {
        return $this->customFormThemeAction($request);
    }

    /**
     * Advanced usage
     *
     * Collection of collections are useful on the most dynamic forms, and a good
     * way to test if the plugin is working as expected too.
     *
     * @Route("/collectionOfCollections", name="collectionOfCollections")
     * @Template()
     */
    public function collectionOfCollectionsAction(Request $request)
    {
        $data = [
            'collections' => [
                [new Value('a'), new Value('b'), new Value('c')],
                [new Value('d'), new Value('e'), new Value('f')],
                [new Value('g'), new Value('h'), new Value('i')],
            ],
        ];

        $form = $this
           ->get('form.factory')
           ->createNamedBuilder('form', Type\FormType::class, $data)
           ->add('collections', Type\CollectionType::class, [
               'entry_type'    => Type\CollectionType::class,
               'label'         => 'Add, move, remove collections',
               'entry_options' => [
                   'entry_type'    => ValueType::class,
                   'label'         => 'Add, move, remove values',
                   'entry_options' => [
                       'label' => 'Value',
                   ],
                   'allow_add'      => true,
                   'allow_delete'   => true,
                   'prototype'      => true,
                   'prototype_name' => '__children_name__',
                   'attr'           => [
                       'class' => 'child-collection',
                   ],
               ],
               'allow_add'      => true,
               'allow_delete'   => true,
               'prototype'      => true,
               'prototype_name' => '__parent_name__',
               'attr'           => [
                   'class' => 'parent-collection',
               ],
           ])
           ->add('submit', Type\SubmitType::class)
           ->getForm()
        ;

        $form->handleRequest($request);
        if ($form->isValid()) {
            $data = $form->getData();
        }

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

    /**
     * Advanced usage
     *
     * Looks like there are weird behaviours with Doctrine:
     * https://github.com/ninsuo/symfony-collection/issues/7
     * Let's test that live!
     *
     * ... hmm, doesn't look bad anyway
     *
     * @Route(
     *      "/usageWithDoctrine/{name}",
     *      name = "usageWithDoctrine",
     *      defaults = {
     *          "name" = "example"
     *      }
     * )
     * @Template()
     */
    public function usageWithDoctrineAction(Request $request, $name)
    {
        $repo = $this->getDoctrine()->getRepository('FuzAppBundle:MyArray');

        $data = $repo->findOneByName($name);
        if (is_null($data)) {
            $data = $repo->create($name);
        }

        $form = $this->createForm(MyArrayType::class, $data);
        $form->handleRequest($request);

        $form->get('save')->isClicked() && $form->isValid() && $repo->save($data);

        return [
            'names' => $repo->getArrayNames(),
            'form'  => $form->createView(),
            'data'  => $data,
        ];
    }

    /**
     * Related to usageWithDoctrine demo
     *
     * @Route(
     *      "/usageWithDoctrineDelete/{name}",
     *      name = "usageWithDoctrineDelete"
     * )
     */
    public function usageWithDoctrineDeleteAction(Request $request, $name)
    {
        $repo = $this->getDoctrine()->getRepository('FuzAppBundle:MyArray');
        if (!is_null($data = $repo->findOneByName($name))) {
            $repo->delete($data);
        }

        return $this->forward('FuzAppBundle:Advanced:usageWithDoctrine', [
            'name' => 'example',
        ]);
    }

    /**
     * A form having a theme and containing several fields
     *
     * @Route(
     *      "/formHavingSeveralFields",
     *      name = "formHavingSeveralFields"
     * )
     * @Template()
     */
    public function formHavingSeveralFieldsAction(Request $request)
    {
        $address = new Address();
        $address->setName('Mickael Steller');
        $address->setCompany('fuz.org');
        $address->setStreet('41 rue de la Paix');
        $address->setPostalcode('75002');
        $address->setCity('Paris');
        $address->setCountry('France');

        $addresses = new Addresses();
        $addresses->getAddresses()->add($address);

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

        return [
            'form' => $form->createView(),
            'data' => $addresses,
        ];
    }

    /**
     * Another example of form theme
     *
     * @Route(
     *      "/fancyFormTheme",
     *      name = "fancyFormTheme"
     * )
     * @Template()
     */
    public function fancyFormThemeAction(Request $request)
    {
        $fancyCollection = new FancyCollection();

        for ($i = 0; $i < 3; $i++) {
            $fancy = new Fancy();
            $fancyCollection->getFancyCollection()->add($fancy);
        }

        $form = $this->createForm(FancyCollectionType::class, $fancyCollection);

        $form->handleRequest($request);
        if ($form->isValid()) {
            $data = $form->getData();
        }

        return [
            'form' => $form->createView(),
            'data' => $fancyCollection,
        ];
    }
}
File: Controller/AdvancedController.php
File: Entity/Address.php
<?php

namespace Fuz\AppBundle\Entity;

class Address
{
    protected $name;
    protected $company;
    protected $street;
    protected $postalcode;
    protected $city;
    protected $country;

    public function getName()
    {
        return $this->name;
    }

    public function getCompany()
    {
        return $this->company;
    }

    public function getStreet()
    {
        return $this->street;
    }

    public function getPostalcode()
    {
        return $this->postalcode;
    }

    public function getCity()
    {
        return $this->city;
    }

    public function getCountry()
    {
        return $this->country;
    }

    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    public function setCompany($company)
    {
        $this->company = $company;

        return $this;
    }

    public function setStreet($street)
    {
        $this->street = $street;

        return $this;
    }

    public function setPostalcode($postalcode)
    {
        $this->postalcode = $postalcode;

        return $this;
    }

    public function setCity($city)
    {
        $this->city = $city;

        return $this;
    }

    public function setCountry($country)
    {
        $this->country = $country;

        return $this;
    }
}
File: Entity/Address.php
File: Entity/Addresses.php
<?php

namespace Fuz\AppBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;

class Addresses
{
    protected $addresses;

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

    public function getAddresses()
    {
        return $this->addresses;
    }
}
File: Entity/Addresses.php
File: Form/AddressType.php
<?php

namespace Fuz\AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class AddressType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name');
        $builder->add('company');
        $builder->add('street');
        $builder->add('postalcode');
        $builder->add('city');
        $builder->add('country');
    }

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

    public function getBlockPrefix()
    {
        return 'AddressType';
    }
}
File: Form/AddressType.php
File: Form/AddressesType.php
<?php

namespace Fuz\AppBundle\Form;

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\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class AddressesType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('addresses', CollectionType::class, [
            'label'        => 'Address',
            'entry_type'   => AddressType::class,
            'allow_add'    => true,
            'allow_delete' => true,
            'prototype'    => true,
            'required'     => false,
            'by_reference' => true,
            'delete_empty' => true,
            'attr'         => [
                'class' => 'collection',
            ],
        ]);

        $builder->add('save', SubmitType::class, [
                'label' => 'See my addresses',
        ]);
    }

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

    public function getBlockPrefix()
    {
        return 'AddressesType';
    }
}
File: Form/AddressesType.php
File: Resources/views/Advanced/addresses-theme.html.twig

{% block AddressesType_label %}{% endblock %}
{% block AddressesType_errors %}{% endblock %}

{% block AddressType_label %}{% endblock %}
{% block AddressType_errors %}{% endblock %}

{% block AddressType_row %}
<div class="col-md-3">
  {{ form_label(form) }}
  {{ form_errors(form) }}
  {{ form_widget(form) }}
</div>
{% endblock %}

{% block AddressType_widget %}
    {{ form_widget(form) }}
    <br/>
    <p class="text-center">
        <a href="#" class="collection-up btn btn-default">&lt;</a>
        <a href="#" class="collection-remove btn btn-default">-</a>
        <a href="#" class="collection-add btn btn-default">+</a>
        <a href="#" class="collection-down btn btn-default">&gt;</a>
    </p>
{% endblock %}

{% block AddressesType_widget %}
    {{ form_widget(form.addresses) }}
{% endblock %}
File: Resources/views/Advanced/addresses-theme.html.twig
File: Resources/views/Advanced/formHavingSeveralFields.html.twig
{% extends 'FuzAppBundle::layout.html.twig' %}

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

{% block title %}Advanced usage: custom theme and several fields{% endblock %}

{% block body %}

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

    <p>
        An example that shows how works theming on a form with more than one field.
    </p>

    <div class="row">
        {%
            form_theme form
                'jquery.collection.html.twig'
                'FuzAppBundle:Advanced:addresses-theme.html.twig'
        %}
        {{ form(form) }}
    </div>

    {% for address in data.addresses %}
        <p>
            {{ address.name }} at {{ address.company }}, {{ address.street }}, {{ address.postalcode }} {{ address.city }}, {{ address.country }}
        </p>
    {% endfor %}

    {{
        tabs([
            'Base/BaseController.php',
            'Controller/AdvancedController.php',
            'Resources/views/Advanced/addresses-theme.html.twig',
            'Resources/views/Advanced/formHavingSeveralFields.html.twig',
            'Entity/Addresses.php',
            'Entity/Address.php',
            'Form/AddressesType.php',
            'Form/AddressType.php',
        ])
    }}

{% endblock %}

{% block script %}

    <script type="text/javascript">
        $('.collection').collection({
            'drag_drop_options': {
                'placeholder': null
            }
        });
    </script>

{% endblock %}
File: Resources/views/Advanced/formHavingSeveralFields.html.twig