Skip to content

Commit

Permalink
feature #394 Allow to define custom field types and options on-the-fl…
Browse files Browse the repository at this point in the history
…y (javiereguiluz, ogizanagi)

This PR was merged into the master branch.

Discussion
----------

Allow to define custom field types and options on-the-fly

This PR is based on the great work done by @ogizanagi in #379. All credit goes for him. I've just done some minor refactorings.

TODO:

  * Solve the ` if ($this->debug) { throw $e; }` in the Twig template.
  * Add tests.

Commits
-------

a0edb9c Added more tests
cd2b770 Removed an unneeded argument
4d0a34e Fixed a minor typo in one error message
77ccd42 Added more tests for the new feature
e55fad2 Reworded the documentation of the new feature
55db338 Fixed a minor error and added one test
7ed6b74 Fixed minor typos
c7e734a Updated the documentation for the new feature
fe745cc Introduced the "template" field option to define a custom template to render the field
64f88cd Minor tweaks
3adf8d5 Removed an unneeded variableº
5b7f4a7 Fixed some errors
8a4be07 Renamed fieldMetadata template variable by field_options
ced2cae Removed an unneeded variable after the last change
886daf0 Refactored the code that looks for the custom field types templates
149f806 Throw an error when the entity uses a custom on-the-fly type without defining its related template
a9203fa Allow custom fields dataTypes on-the-fly and pass fieldMetadata to templates.
3b210a2 Allows field templates to be customized by field name
  • Loading branch information
javiereguiluz committed Jul 20, 2015
2 parents 6e43f30 + a0edb9c commit f4dd66e
Show file tree
Hide file tree
Showing 25 changed files with 378 additions and 47 deletions.
37 changes: 37 additions & 0 deletions Configuration/Configurator.php
Expand Up @@ -34,6 +34,7 @@ class Configurator
'dataType' => null, // Data type (text, date, integer, boolean, ...) of the Doctrine property associated with the field
'virtual' => false, // is a virtual field or a real Doctrine entity property?
'sortable' => true, // listings can be sorted according to the values of this field
'template' => null, // the path of the template used to render the field in 'show' and 'list' views
);

private $doctrineTypeToFormTypeMap = array(
Expand Down Expand Up @@ -104,6 +105,8 @@ public function getEntityConfiguration($entityName)

$entityConfiguration = $this->introspectGettersAndSetters($entityConfiguration);

$entityConfiguration = $this->processFieldTemplates($entityConfiguration);

$this->entitiesConfig[$entityName] = $entityConfiguration;

return $entityConfiguration;
Expand Down Expand Up @@ -458,6 +461,40 @@ private function introspectGettersAndSetters($entityConfiguration)
return $entityConfiguration;
}


/**
* Determines the template used to render each backend element. This is not
* trivial because templates can depend on the entity displayed and they
* define an advanced override mechanism.
*
* @param array $entityConfiguration
*
* @return array
*/
private function processFieldTemplates(array $entityConfiguration)
{
foreach (array('list', 'show') as $view) {
foreach ($entityConfiguration[$view]['fields'] as $fieldName => $fieldMetadata) {
if (null !== $fieldMetadata['template']) {
continue;
}

// this prevents the template from displaying the 'id' primary key formatted as a number
if ('id' === $fieldName) {
$template = $entityConfiguration['templates']['field_id'];
} elseif (array_key_exists('field_'.$fieldMetadata['type'], $entityConfiguration['templates'])) {
$template = $entityConfiguration['templates']['field_'.$fieldMetadata['type']];
} else {
$template = $entityConfiguration['templates']['label_undefined'];
}

$entityConfiguration[$view]['fields'][$fieldName]['template'] = $template;
}
}

return $entityConfiguration;
}

/**
* Returns the most appropriate Symfony Form type for the given Doctrine type.
*
Expand Down
47 changes: 43 additions & 4 deletions DependencyInjection/EasyAdminExtension.php
Expand Up @@ -18,6 +18,8 @@

class EasyAdminExtension extends Extension
{
private $views = array('edit', 'list', 'new', 'show');

private $defaultActionConfiguration = array(
'name' => null, // either the name of a controller method or an application route (it depends on the 'type' option)
'type' => 'method', // 'method' if the action is a controller method; 'route' if it's an application route
Expand Down Expand Up @@ -201,7 +203,7 @@ public function processEntityActions(array $backendConfiguration)
$entityConfiguration['disabled_actions'] = $disabledActions;

// second, define the actions of each entity view
foreach (array('edit', 'list', 'new', 'show') as $view) {
foreach ($this->views as $view) {
$defaultActions = $this->getDefaultActions($view);
$backendActions = isset($backendConfiguration[$view]['actions']) ? $backendConfiguration[$view]['actions'] : array();
$backendActions = $this->normalizeActionsConfiguration($backendActions, $defaultActions);
Expand Down Expand Up @@ -392,8 +394,9 @@ private function filterRemovedActions(array $actionsConfiguration)
*/
private function processEntityTemplates(array $backendConfiguration)
{
$applicationTemplateDir = $this->kernelRootDir.'/Resources/views';
$templatesDir = $this->kernelRootDir.'/Resources/views';

// first, resolve the general template overriding mechanism
foreach ($backendConfiguration['entities'] as $entityName => $entityConfiguration) {
foreach ($this->defaultBackendTemplates as $templateName => $defaultTemplatePath) {
// 1st level priority: easy_admin.entities.<entityName>.templates.<templateName> config option
Expand All @@ -403,10 +406,10 @@ private function processEntityTemplates(array $backendConfiguration)
} elseif (isset($backendConfiguration['design']['templates'][$templateName])) {
$template = $backendConfiguration['design']['templates'][$templateName];
// 3rd level priority: app/Resources/views/easy_admin/<entityName>/<templateName>.html.twig
} elseif (file_exists($applicationTemplateDir.'/easy_admin/'.$entityName.'/'.$templateName.'.html.twig')) {
} elseif (file_exists($templatesDir.'/easy_admin/'.$entityName.'/'.$templateName.'.html.twig')) {
$template = 'easy_admin/'.$entityName.'/'.$templateName.'.html.twig';
// 4th level priority: app/Resources/views/easy_admin/<templateName>.html.twig
} elseif (file_exists($applicationTemplateDir.'/easy_admin/'.$templateName.'.html.twig')) {
} elseif (file_exists($templatesDir.'/easy_admin/'.$templateName.'.html.twig')) {
$template = 'easy_admin/'.$templateName.'.html.twig';
// 5th level priority: @EasyAdmin/default/<templateName>.html.twig
} else {
Expand All @@ -419,6 +422,42 @@ private function processEntityTemplates(array $backendConfiguration)
$backendConfiguration['entities'][$entityName] = $entityConfiguration;
}

// second, walk through all entity fields to determine their specific template
foreach ($backendConfiguration['entities'] as $entityName => $entityConfiguration) {
foreach (array('list', 'show') as $view) {
foreach ($entityConfiguration[$view]['fields'] as $fieldName => $fieldMetadata) {
// if the field defines its own template, resolve its location
if (isset($fieldMetadata['template'])) {
$templateName = $fieldMetadata['template'];

// template name should not contain the .html.twig extension
// however, for usability reasons, we silently fix this issue if needed
if ('.html.twig' === substr($templateName, -10)) {
$templateName = substr($templateName, 0, -10);
}

// 1st level priority: app/Resources/views/easy_admin/<entityName>/<templateName>.html.twig
if (file_exists($templatesDir.'/easy_admin/'.$entityName.'/'.$templateName.'.html.twig')) {
$templatePath = 'easy_admin/'.$entityName.'/'.$templateName.'.html.twig';
// 2nd level priority: app/Resources/views/easy_admin/<templateName>.html.twig
} elseif (file_exists($templatesDir.'/easy_admin/'.$templateName.'.html.twig')) {
$templatePath = 'easy_admin/'.$templateName.'.html.twig';
} else {
throw new \RuntimeException(sprintf('The "%s" field of the "%s" entity uses a custom template called "%s" which doesn\'t exist in "app/Resources/views/easy_admin/" directory.', $fieldName, $entityName, $templateName));
}
} else {
// At this point, we don't know the exact data type associated with each field.
// The template is initialized to null and it will be resolved at runtime in the Configurator class
$templatePath = null;
}

$entityConfiguration[$view]['fields'][$fieldName]['template'] = $templatePath;
}
}

$backendConfiguration['entities'][$entityName] = $entityConfiguration;
}

return $backendConfiguration;
}

Expand Down
1 change: 1 addition & 0 deletions Resources/config/services.xml
Expand Up @@ -7,6 +7,7 @@

<service id="easyadmin.twig.extension" class="JavierEguiluz\Bundle\EasyAdminBundle\Twig\EasyAdminTwigExtension" public="false">
<argument type="service" id="easyadmin.configurator"></argument>
<argument>%kernel.debug%</argument>
<tag name="twig.extension" />
</service>

Expand Down
10 changes: 10 additions & 0 deletions Resources/doc/getting-started/4-views-and-actions.md
Expand Up @@ -296,6 +296,9 @@ These are the options that you can define for each field:
using the default Bootstrap based form theme, this value is applied to the
`<div class="form-group">` element which wraps the label, the widget and
the error messages of the field.
* `template` (optional): the name of the custom template used to render the
contents of the field in the `list` and `show` views. This option is fully
explained in the [Advanced Design Customization] [advanced-design-customization] tutorial.
* `type` (optional): the type of data displayed in the `list`, `search` and
`show` views and the form widget displayed in the `edit` and `new` views.
These are the supported types:
Expand All @@ -308,6 +311,12 @@ These are the options that you can define for each field:
* `toggle`, displays a boolean value as a flip switch in the `list`
and `search` views (as explained later in this chapter).

In addition to these "official" options, you can define any custom option for
the fields. These custom options are passed to the template that renders each
field, allowing to create very powerful backend customizations, as explained
in the [Advanced Design Customization] [advanced-design-customization]
tutorial.

### Translate Property Labels

Before translating the labels, make sure that the `translator` service is
Expand Down Expand Up @@ -653,3 +662,4 @@ article of the official Symfony documentation to learn how to define custom
form types.

[custom-actions]: ../tutorials/customizing-backend-actions.md
[advanced-design-customization]: ../tutorials/advanced-design-customization.md
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 109 additions & 4 deletions Resources/doc/tutorials/advanced-design-customization.md
Expand Up @@ -281,12 +281,117 @@ about their features.
Inside the `field_*` and `label_*` templates you have access to the following
variables:

* `view`, the name of the view where the field is being rendered (`show` or
`list`).
* `field_options`, the options configured for this field in the backend
configuration file.
* `item`, the entity instance.
* `value`, the content of the field being rendered, which can be a variable
of any type (string, numeric, boolean, array, etc.)
* `format`, available only for the date and numeric field types. It defines
the formatting that should be applied to the value before displaying it.
* `view`, the name of the view where the field is being rendered (`show` or
`list`).

### Rendering Properties with Custom Templates

The default property templates are flexible enough for most backends. However,
when your backend is very complex, it may be useful to use a custom template to
define the way some property is rendered in the `list` or `show` views.

To do so, define the name of the custom template in the `template` option of
the property:

```yaml
easy_admin:
# ...
entities:
Invoice:
list:
fields:
- { property: 'total', template: 'invoice_total' }
```

The above configuration makes the backend use the `invoice_total.html.twig`
template instead of the default `field_float.html.twig` template. Custom
templates are looked for in the following locations (the first existing
template is used):

1. `app/Resources/views/easy_admin/<EntityName>/<TemplateName>.html.twig`
template.
2. `app/Resources/views/easy_admin/<TemplateName>.html.twig`
template.

Custom templates receive the same parameters as built-in templates (
`field_options`, `item`, `value`, `view`).

### Adding Custom Logic to Property Templates

All property templates receive a parameter called `field_options` with the full
list of options defined in the configuration file for that property. If you
add custom options, they will also be available in the `field_options`
parameter. This allows you to add custom logic to templates very easily.

Imagine that you want to translate some text contents in the `list` view. To do
so, define a custom option called `trans` which indicates if the property
content should be translated and another option called `domain` which defines
the name of the translation domain to use.

```yaml
# app/config.yml
Product:
class: AppBundle\Entity\Product
label: 'Products'
list:
fields:
- id
- { property: 'name', trans: true, domain: 'messages' }
# ...
```

Supposing that the `name` property is of type `string`, you just need to
override the built-in `field_string.html.twig` template:

```twig
{# app/Resources/views/easy_admin/field_string.html.twig #}
{% if field_options.trans|default(false) %}
{# translate fields defined as "translatable" #}
{{ value|trans({}, field_options.domain|default('messages')) }}
{% else %}
{# if not translatable, simply include the default template #}
{{ include('@EasyAdmin/default/field_string.html.twig') }}
{% endif %}
```

If the custom logic is too complex, it may be better to use your own custom
template to not mess built-in templates too much. In this example, the
collection of tags associated with a product is displayed in a way that is too
customized to use a built-in template:

```yaml
# app/config.yml
Product:
class: AppBundle\Entity\Product
label: 'Products'
list:
fields:
- id
# ...
- { property: 'tags', type: 'tag_collection', label_colors: ['primary', 'success', 'info'] }
```

The custom `tag_collection.html.twig` would look as follows:

```twig
{# app/Resources/views/easy_admin/tag_collection.html.twig #}
{% set colors = field_options.label_colors|default(['primary']) %}
{% for tag in value %}
<span class="label label-{{ cycle(colors, loop.index) }}">{{ tag }}</span>
{% endfor %}
```

And this property would be rendered in the `list` view as follows:

![Default listing interface](../images/easyadmin-design-customization-custom-data-types.png)

Translating Backend Elements in Custom Templates
------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions Resources/views/default/field_bigint.html.twig
@@ -1,5 +1,5 @@
{% if format %}
{{ format|format(value) }}
{% if field_options.format %}
{{ field_options.format|format(value) }}
{% else %}
{{ value|number_format }}
{% endif %}
2 changes: 1 addition & 1 deletion Resources/views/default/field_date.html.twig
@@ -1 +1 @@
{{ value|date(format) }}
{{ value|date(field_options.format) }}
2 changes: 1 addition & 1 deletion Resources/views/default/field_datetime.html.twig
@@ -1 +1 @@
{{ value|date(format) }}
{{ value|date(field_options.format) }}
2 changes: 1 addition & 1 deletion Resources/views/default/field_datetimez.html.twig
@@ -1 +1 @@
{{ value|date(format) }}
{{ value|date(field_options.format) }}
4 changes: 2 additions & 2 deletions Resources/views/default/field_decimal.html.twig
@@ -1,5 +1,5 @@
{% if format %}
{{ format|format(value) }}
{% if field_options.format %}
{{ field_options.format|format(value) }}
{% else %}
{{ value|number_format(2) }}
{% endif %}
4 changes: 2 additions & 2 deletions Resources/views/default/field_float.html.twig
@@ -1,5 +1,5 @@
{% if format %}
{{ format|format(value) }}
{% if field_options.format %}
{{ field_options.format|format(value) }}
{% else %}
{{ value|number_format(2) }}
{% endif %}
4 changes: 2 additions & 2 deletions Resources/views/default/field_integer.html.twig
@@ -1,5 +1,5 @@
{% if format %}
{{ format|format(value) }}
{% if field_options.format %}
{{ field_options.format|format(value) }}
{% else %}
{{ value|number_format }}
{% endif %}
4 changes: 2 additions & 2 deletions Resources/views/default/field_smallint.html.twig
@@ -1,5 +1,5 @@
{% if format %}
{{ format|format(value) }}
{% if field_options.format %}
{{ field_options.format|format(value) }}
{% else %}
{{ value|number_format }}
{% endif %}
2 changes: 1 addition & 1 deletion Resources/views/default/field_time.html.twig
@@ -1 +1 @@
{{ value|date(format) }}
{{ value|date(field_options.format) }}

0 comments on commit f4dd66e

Please sign in to comment.