crud-view
crud-view copied to clipboard
Best way to customize relation display fields
Recently had a requirement to display a shorter name for a related field in the index table (from the default name
field to one called short_name
).
This is the most elegant solution I could come up with:
public function index()
{
$action = $this->Crud->action();
$action->config([
'scaffold.fields' => [
'facility_id' => [
'formatter' => function ($name, $value, $entity) {
return $this->getView()->Html->link(
$entity->facility->short_name,
['controller' => 'Facilities', 'action' => 'view', $entity->facility->id]
);
}
],
Other things I tried included:
- Using an element formatter (though getting all the information I needed to create the link in a reusable way, such as the target controller name, wasn't pretty so I abandoned that idea.)
- Changing the display field on the association (we needed the full name in the search filters, so this involved changing the displayField in
beforePaginate
and back again inrelatedModel
, which wasn't ideal either.)
Is this a common enough use case where it should be configurable like like title
and label
?
public function index()
{
$action = $this->Crud->action();
$action->config([
'scaffold.fields' => [
'facility_id' => ['displayField' => 'short_name'],
I think a good way is using the element formatter:
'scaffold.fields' => ['facility_id' => ['formatter' => 'element', 'element' => 'facility']]
Then the Element/facility.ctp
will be rendered and you have full control over what is displayed.
Derp, I took the time to read the full issue description, you are already using that. That said, I'm not sold on complicating the formatting options by using an array DSL.
I'm open to see what others think about it.
@deizel Can't your particular case be handled just by changing display field of Facilities
table to short_name
and then specifying 'facility_id' => ['formatter' => 'relation']
in scaffold.fields
?
@ADmad Unfortunately, since I need the name
elsewhere (e.g. search filters at the top of index, and drop downs on add/edit), setting the default display field to short_name
just moves the problem elsewhere:
public function index()
{
$this->Crud->on('relatedModel', function(Event $event) {
if ($event->subject->name === 'Facilities') {
$event->subject->query = $this->Accounts->Facilities->find('list', [
'valueField' => 'name'
]);
}
});
(Side note: $event->subject->query()
already has the 'list' finder attached, so trying doing $event->subject->query()->find('list', ['valueField' => 'name'])
instead gives you an array with null values.)
Also I failed to mention earlier, when the default display field was name
, trying to dynamically change the display field to short_name
in beforePaginate
and then back to name
in the afterPaginate
and relatedModel
didn't work out either. Seems these events fire before PaginatorComponent::paginate()
, and trying to execute the query early causes it to throw an exception (since a query object is expected).
Ultimately, this leads me back to 'formatter' being the only direct solution, with 'callback' being less convoluted than 'element', and 'relation' not being configurable enough.
@lorenzo For reference, this was the approach with a reusable entity (after cleaning it up):
public function index()
{
$action = $this->Crud->action();
$action->config([
'scaffold.fields' => [
'facility_id' => [
'formatter' => 'element',
'element' => 'index/relation',
'assocKey' => 'facility',
'displayField' => 'short_name',
'controller' => 'Facilities',
],
src/Template/Element/index/relation.ctp
:
<?php
echo $this->Html->link(
$context->$options['assocKey']->$options['displayField'],
['controller' => $options['controller'], 'action' => 'view', $value]
);
Whatever we decide, it might be worth documenting the suggested approach to save others time. :wink:
Well then imo yours is pretty specific use case and using a formatter element or callback is the way to go. You can help documenting it by providing a patch for the docs.
Just a quick update to say I haven't forgot about this and plan to get around to it at some point.
My solution currently looks like this for now, after a few iterations:
public function index()
{
$action = $this->Crud->action();
$action->config([
'scaffold.fields' => [
'facility_id' => [
'formatter' => 'element',
'element' => 'index/relation',
'displayField' => 'short_name',
'controller' => 'Facilities',
],
'account_status_id' => [
'formatter' => 'element',
'element' => 'index/relation',
'link' => false,
],
src/Template/Element/index/relation.ctp
:
<?php
use Cake\Utility\Hash;
$link = Hash::get($options, 'link', true);
$displayField = Hash::get($options, 'displayField', 'name');
$assocKey = Hash::get($options, 'assocKey', str_replace('_id', '', $field));
$assoc = $context->$assocKey;
if ($assoc):
$name = $assoc->$displayField;
if ($link):
echo $this->Html->link($name, ['controller' => $options['controller'], 'action' => 'view', $value]);
else:
echo $name;
endif;
endif;
If I remember correctly, this now handles the following cases:
- you want a different display field
- and provide all the values the element doesn't know (
displayField
,controller
,assocKey
) - you don't want to pass in
displayField
, because it's justname
. - you don't want to pass in
assocKey
, and would rather the element guessed. - (you need to pass in the
controller
for the link, because I haven't bothered with inflection)
- and provide all the values the element doesn't know (
- the value at
assocKey
is null and you don't want an empty link - you don't want a link at all (say, for example, there is no controller to link to)
I personally think the helper should be able to support all of this, but I'm still open to suggestions.