Skip to content

Commit

Permalink
feature #1214 Allow to create complex form layouts (javiereguiluz)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the master branch (closes #1214).

Discussion
----------

Allow to create complex form layouts

### Context

These past days I had to use Magento 2. I liked how they present the forms in their backend, so I want to make EasyAdmin capable of creating similar layouts:

![magento_form](https://cloud.githubusercontent.com/assets/73419/16241664/03042e84-37ef-11e6-986e-ff0bb66e7008.png)

### Solution

I propose to define three new design elements for forms: "dividers", "fieldsets" and "sections" (names are temporary).

1) A `divider` is just a `<hr>`

2) A `section` is similar to the Magento design:

![fieldset](https://cloud.githubusercontent.com/assets/73419/16241835/cd3e7f60-37ef-11e6-890c-5077c7fefb47.png)

3) A `group` is what Sonata calls `groups`:

(the screenshot is not great, but this is a really nice Sonata feature; it's very useful and for real forms it looks beautiful)

![sonata_form](https://cloud.githubusercontent.com/assets/73419/16241848/e20c3950-37ef-11e6-97bf-24a3e0c54a74.png)

### Proposal

This PR contains:

* A first draft of the docs to explain this new feature
* A silly proof of concept to implement the "divider" type.

Commits
-------

2c7e2dc Allow to create complex form layouts
  • Loading branch information
javiereguiluz committed Jul 5, 2016
2 parents bef27aa + 2c7e2dc commit 2e66ee4
Show file tree
Hide file tree
Showing 167 changed files with 1,423 additions and 190 deletions.
73 changes: 66 additions & 7 deletions Configuration/NormalizerConfigPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ public function __construct(ContainerInterface $container)
public function process(array $backendConfig)
{
$backendConfig = $this->normalizeEntityConfig($backendConfig);
$backendConfig = $this->normalizeFormViewConfig($backendConfig);
$backendConfig = $this->normalizeFormConfig($backendConfig);
$backendConfig = $this->normalizeViewConfig($backendConfig);
$backendConfig = $this->normalizePropertyConfig($backendConfig);
$backendConfig = $this->normalizeFormDesignConfig($backendConfig);
$backendConfig = $this->normalizeActionConfig($backendConfig);
$backendConfig = $this->normalizeControllerConfig($backendConfig);

Expand Down Expand Up @@ -74,7 +75,7 @@ private function normalizeEntityConfig(array $backendConfig)
*
* @return array
*/
private function normalizeFormViewConfig(array $backendConfig)
private function normalizeFormConfig(array $backendConfig)
{
foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
if (isset($entityConfig['form'])) {
Expand Down Expand Up @@ -148,7 +149,7 @@ private function normalizePropertyConfig(array $backendConfig)
foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
foreach (array('edit', 'list', 'new', 'search', 'show') as $view) {
$fields = array();
foreach ($entityConfig[$view]['fields'] as $field) {
foreach ($entityConfig[$view]['fields'] as $i => $field) {
if (!is_string($field) && !is_array($field)) {
throw new \RuntimeException(sprintf('The values of the "fields" option for the "%s" view of the "%s" entity can only be strings or arrays.', $view, $entityConfig['class']));
}
Expand All @@ -158,9 +159,9 @@ private function normalizePropertyConfig(array $backendConfig)
$fieldConfig = array('property' => $field);
} else {
// Config format #1: field is an array that defines one or more
// options. Check that the mandatory 'property' option is set
if (!array_key_exists('property', $field)) {
throw new \RuntimeException(sprintf('One of the values of the "fields" option for the "%s" view of the "%s" entity does not define the "property" option.', $view, $entityConfig['class']));
// options. Check that either 'property' or 'type' option is set
if (!array_key_exists('property', $field) && !array_key_exists('type', $field)) {
throw new \RuntimeException(sprintf('One of the values of the "fields" option for the "%s" view of the "%s" entity does not define neither of the mandatory options ("property" or "type").', $view, $entityConfig['class']));
}

$fieldConfig = $field;
Expand All @@ -174,7 +175,8 @@ private function normalizePropertyConfig(array $backendConfig)
}
}

$fieldName = $fieldConfig['property'];
// fields that don't define the 'property' name are special form design elements
$fieldName = isset($fieldConfig['property']) ? $fieldConfig['property'] : '_easyadmin_form_design_element_'.$i;
$fields[$fieldName] = $fieldConfig;
}

Expand All @@ -185,6 +187,63 @@ private function normalizePropertyConfig(array $backendConfig)
return $backendConfig;
}

/**
* Normalizes the configuration of the special elements that forms may include
* to create advanced designs (such as dividers and fieldsets).
*
* @param array $backendConfig
*
* @return array
*/
private function normalizeFormDesignConfig(array $backendConfig)
{
// edge case: if the first 'group' type is not the first form field,
// all the previous form fields are "ungrouped". To avoid design issues,
// insert an empty 'group' type (no label, no icon) as the first form element.
foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
foreach (array('edit', 'new') as $view) {
$fieldNumber = 0;
$isTheFirstGroupElement = true;

foreach ($entityConfig[$view]['fields'] as $fieldName => $fieldConfig) {
++$fieldNumber;
if (!isset($fieldConfig['property']) && isset($fieldConfig['type']) && 'group' === $fieldConfig['type']) {
if ($isTheFirstGroupElement && $fieldNumber > 1) {
$backendConfig['entities'][$entityName][$view]['fields'] = array_merge(
array('_easyadmin_form_design_element_forced_first_group' => array('type' => 'group')),
$backendConfig['entities'][$entityName][$view]['fields']
);

break;
}

$isTheFirstGroupElement = false;
}
}
}
}

foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
foreach (array('edit', 'new') as $view) {
foreach ($entityConfig[$view]['fields'] as $fieldName => $fieldConfig) {
// this is a form design element instead of a regular property
$isFormDesignElement = !isset($fieldConfig['property']) && isset($fieldConfig['type']);
if ($isFormDesignElement && in_array($fieldConfig['type'], array('divider', 'group', 'section'))) {
// assign them a property name to add them later as unmapped form fields
$fieldConfig['property'] = $fieldName;

// transform the form type shortcuts into the real form type short names
$fieldConfig['type'] = 'easyadmin_'.$fieldConfig['type'];
}

$backendConfig['entities'][$entityName][$view]['fields'][$fieldName] = $fieldConfig;
}
}
}

return $backendConfig;
}

private function normalizeActionConfig(array $backendConfig)
{
$views = array('edit', 'list', 'new', 'show');
Expand Down
2 changes: 2 additions & 0 deletions Configuration/PropertyConfigPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class PropertyConfigPass implements ConfigPassInterface
'template' => null,
// the options passed to the Symfony Form type used to render the form field
'type_options' => array(),
// the name of the group where this form field is displayed (used only for complex form layouts)
'form_group' => null,
);

private $defaultVirtualFieldMetadata = array(
Expand Down
29 changes: 29 additions & 0 deletions Form/Type/EasyAdminDividerType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace JavierEguiluz\Bundle\EasyAdminBundle\Form\Type;

use Symfony\Component\Form\AbstractType;

/**
* The 'divider' form type is a special form type used to display a design
* element needed to create complex form layouts. This "fake" type just displays
* some HTML tags and it must be added to a form as "unmapped" and "non required".
*/
class EasyAdminDividerType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'easyadmin_divider';
}

/**
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
}
43 changes: 40 additions & 3 deletions Form/Type/EasyAdminFormType.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use JavierEguiluz\Bundle\EasyAdminBundle\Form\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
Expand Down Expand Up @@ -53,6 +55,8 @@ public function buildForm(FormBuilderInterface $builder, array $options)
$view = $options['view'];
$entityConfig = $this->configManager->getEntityConfig($entity);
$entityProperties = $entityConfig[$view]['fields'];
$formGroups = array();
$currentFormGroup = null;

foreach ($entityProperties as $name => $metadata) {
$formFieldOptions = $metadata['type_options'];
Expand All @@ -65,8 +69,39 @@ public function buildForm(FormBuilderInterface $builder, array $options)
}

$formFieldType = LegacyFormHelper::getType($metadata['fieldType']);

// if the form field is a special 'group' design element, don't add it
// to the form. Instead, consider it the current form group (this is
// applied to the form fields defined after it) and store its details
// in a property to get them in form template
if (in_array($formFieldType, array('easyadmin_group', 'JavierEguiluz\\Bundle\\EasyAdminBundle\\Form\\Type\\EasyAdminGroupType'))) {
$currentFormGroup = $metadata['fieldName'];
$formGroups[$currentFormGroup] = $metadata;

continue;
}

$formFieldOptions['attr']['form_group'] = $currentFormGroup;

// 'divider' and 'section' are 'fake' form fields used to create the design
// elements of the complex form layouts: define them as unmapped and non-required
if (0 === strpos($metadata['property'], '_easyadmin_form_design_element_')) {
$formFieldOptions['mapped'] = false;
$formFieldOptions['required'] = false;
}

$builder->add($name, $formFieldType, $formFieldOptions);
}

$builder->setAttribute('easyadmin_form_groups', $formGroups);
}

/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
$view->vars['easyadmin_form_groups'] = $form->getConfig()->getAttribute('easyadmin_form_groups');
}

/**
Expand All @@ -88,10 +123,12 @@ public function configureOptions(OptionsResolver $resolver)
))
->setRequired(array('entity', 'view'));

if (LegacyFormHelper::useLegacyFormComponent()) {
$resolver->setNormalizers(array('attr' => $this->getAttributesNormalizer()));
} else {
// setNormalizer() is available since Symfony 2.6
if (method_exists($resolver, 'setNormalizer')) {
$resolver->setNormalizer('attr', $this->getAttributesNormalizer());
} else {
// BC for Symfony < 2.6
$resolver->setNormalizers(array('attr' => $this->getAttributesNormalizer()));
}
}

Expand Down
29 changes: 29 additions & 0 deletions Form/Type/EasyAdminGroupType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace JavierEguiluz\Bundle\EasyAdminBundle\Form\Type;

use Symfony\Component\Form\AbstractType;

/**
* The 'group' form type is a special form type used to display a design
* element needed to create complex form layouts. This "fake" type just displays
* some HTML tags and it must be added to a form as "unmapped" and "non required".
*/
class EasyAdminGroupType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'easyadmin_group';
}

/**
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
}
29 changes: 29 additions & 0 deletions Form/Type/EasyAdminSectionType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace JavierEguiluz\Bundle\EasyAdminBundle\Form\Type;

use Symfony\Component\Form\AbstractType;

/**
* The 'section' form type is a special form type used to display a design
* element needed to create complex form layouts. This "fake" type just displays
* some HTML tags and it must be added to a form as "unmapped" and "non required".
*/
class EasyAdminSectionType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'easyadmin_section';
}

/**
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
}
3 changes: 3 additions & 0 deletions Form/Util/LegacyFormHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ final class LegacyFormHelper
'url' => 'Symfony\\Component\\Form\\Extension\\Core\\Type\\UrlType',
// EasyAdmin custom types
'easyadmin_autocomplete' => 'JavierEguiluz\\Bundle\\EasyAdminBundle\\Form\\Type\\EasyAdminAutocompleteType',
'easyadmin_divider' => 'JavierEguiluz\\Bundle\\EasyAdminBundle\\Form\\Type\\EasyAdminDividerType',
'easyadmin_group' => 'JavierEguiluz\\Bundle\\EasyAdminBundle\\Form\\Type\\EasyAdminGroupType',
'easyadmin_section' => 'JavierEguiluz\\Bundle\\EasyAdminBundle\\Form\\Type\\EasyAdminSectionType',
// Popular third-party bundles types
'ckeditor' => 'Ivory\\CKEditorBundle\\Form\\Type\\CKEditorType',
'vich_file' => 'Vich\\UploaderBundle\\Form\\Type\\VichFileType',
Expand Down
12 changes: 12 additions & 0 deletions Resources/config/form.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@
<tag name="form.type" alias="easyadmin_autocomplete" />
</service>

<service id="easyadmin.form.type.divider" class="JavierEguiluz\Bundle\EasyAdminBundle\Form\Type\EasyAdminDividerType">
<tag name="form.type" alias="easyadmin_divider" />
</service>

<service id="easyadmin.form.type.section" class="JavierEguiluz\Bundle\EasyAdminBundle\Form\Type\EasyAdminSectionType">
<tag name="form.type" alias="easyadmin_section" />
</service>

<service id="easyadmin.form.type.group" class="JavierEguiluz\Bundle\EasyAdminBundle\Form\Type\EasyAdminGroupType">
<tag name="form.type" alias="easyadmin_group" />
</service>

<service id="easyadmin.form.type.extension" class="JavierEguiluz\Bundle\EasyAdminBundle\Form\Extension\EasyAdminExtension">
<argument type="service" id="request_stack" on-invalid="null" />
<tag name="form.type_extension" alias="form" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
Expand Down

0 comments on commit 2e66ee4

Please sign in to comment.