Formatters vs Decorators

This chapter describes a common technique used by various components that wish to preserve extensible nature when dealing with used-defined types. Reading this chapter will also help you understand some of the thinking behind major decisions when designing the type system.

When looking into the default money field in Agile UI, which does carry amount, but not the currency, there are a number of considerations when dealing with the field. The first important concept to understand is the distinction between data Presentation and Decoration.

  • Data Presentation: displaying value of the data in a different format, e.g. 123,123.00 vs 123.123,00
  • Data Decoration: adding currency symbol or calendar icon.

Agile UI believes that presentation must be consistent throughout the system. A monetary field will use same format on the Form, Table and even inside a custom HTML template specified into generic View.

When it comes to decoration, the method is very dependent on the context. A form may present Calendar (DatePicker) or enable control icon to indicate currency.

Presentation in Agile Toolkit is handled by Persistence\Ui.

Decoration is performed by helper classes, such as Form\Control\Calendar or Table\Column\Money. The decorator is in control of the final output, so it can decide if it uses the value from presentation or do some decoration on its own.

Extending Data Types

If you are looking to add a new data type, such as “money + currency” combination, which would allow user to specify both the currency and the monetary value, you should start by adding support for a new type.

In the below steps, the #1 and #2 are a minimum to achieve. #3 and #4 will improve experience of your integration.

  1. Extend UI persistence and use your class in $app->uiPersistence.
You need to define how to output your data as well as read it.
  1. Try your new type with a standard Form control.
The value you output should read and stored back correctly. This ensures that standard UI will work with your new data type.
  1. Create your new decorator.
Such as use drop-down to select currency from a pre-defined list inside your specific class while extending Form\Control\Input class. Make sure it can interpret input correctly. The process is explained further down in this chapter.
  1. Associate the types with your decorator.
This happens in Form::controlFactory and Table::decoratorFactory.

For the third party add-ons it is only possible to provide decorators. They must rely on one of the standard types, unless they also offer a dedicated model.

Manually Specifying Decorators

When working with components, they allow to specify decorators manually, even if the type of the field does not seem compatible:

$table->addColumn('field_name', new \Atk4\Ui\Table\Column\Password());

// or

$form->addControl('field_name', new \Atk4\Ui\Form\Control\Password());

Selecting the decorator is done in the following order:

  • specified in second argument to UI addColumn() or addControl() (as shown above)

  • specified using ui property of Atk4DataField:

    $field->ui['form'] = new \Atk4\Ui\Form\Control\Password();
    
  • fallback to Form::controlFactory

Note

When talking about “fields”: you need to know what kind of field you are talking about (Data or UI). Both models (Data) as well as some views (UI: form) use fields. They are not the same. Notably, Model field ui property contains flags like editable, visible and hidden, which do have some impact on rendering, whereas UI field ui property (not used here) designates the Fomantic-UI element to use.

Examples

Let’s explore various use cases and how to properly deal with scenarios

Display password in plain-text for Admin

Normally password is presented as asterisks on the Grid and Form. But what if you want to show it without masking just for the admin? Change type in-line for the model field:

$model = new User($app->db);
$model->getField('password')->type = 'string';

$crud->setModel($model);

Note

Changing element’s type to string will certainly not perform any password encryption.

Hide account_number in specific Table

This is reverse scenario. Field account_number needs to be stored as-is but should be hidden when presented. To hide it from Table:

$model = new User($app->db);

$table->setModel($model);
$model->addDecorator('account_number', new \Atk4\Ui\Table\Column\Password());

Create a decorator for hiding credit card number

If you happen to store card numbers and you only want to display the last digits in tables, yet make it available when editing, you could create your own Table\Column decorator:

class Masker extends \Atk4\Ui\Table\Column
{
    public function getDataCellTemplate(\Atk4\Data\Field $field = null): string
    {
        return '**** **** **** {$mask}';
    }

    public function getHtmlTags(\Atk4\Data\Model $row, ?\Atk4\Data\Field $field): array
    {
        return [
            'mask' => substr($field->get($row), -4),
        ];
    }
}

If you are wondering, why I’m not overriding by providing HTML tag equal to the field name, it’s because this technique is unreliable due to ability to exclude HTML tags with Table::$useHtmlTags.

Display credit card number with spaces

If we always have to display card numbers with spaces, e.g. “1234 1234 1234 1234” but have the database store them without spaces, then this is a data formatting task best done by extending PersistenceUi:

class MyPersistence extends Persistence\Ui
{
    protected function _typecastSaveField(\Atk4\Data\Field $field, $value)
    {
        switch ($field->type) {
            case 'card':
                $parts = str_split($value, 4);

                return implode(' ', $parts);
        }

        return parent::_typecastSaveField($field, $value);
    }

    public function _typecastLoadField(\Atk4\Data\Field $field, $value)
    {
        switch ($field->type) {
            case 'card':
                return str_replace(' ', '', $value);
        }

        return parent::_typecastLoadField($field, $value);
    }
}

class MyApp extends App
{
    public function __construct(array $defaults = [])
    {
        $this->uiPersistence = new MyPersistence()

        parent::__construct($defaults);
    }
}

Now your ‘card’ type will work system-wide.