Custom DGS Fields

This page tells you how to add your own field to a Data Grid Store (DGS). You do not change the framework, the theme, or the system pages.

There are four methods. Use the first method that is sufficient.

Method Use it when Code that you write
Cell view handler Only the value in the list must look different. One plugin method and one template.
Handler field One store needs a special input, and the plugin has the logic. Plugin methods and one template.
Edit view event You must replace the input of a field at run time. One listener.
Field class Many stores or many projects use the field. One class and its templates.

How the DGS finds the class of a field

Each field of a schema has a type. The DGS makes an object of a field class from the type.

  1. If type is the name of a class that implements IStoreField, the DGS uses this class.
  2. If not, the DGS makes the class name: first letter as a capital + Field. Thus text gives TextField, and foreignKey gives ForeignKeyField.

There is no list of registered types. A class that the autoloader can load is sufficient.

<!-- A built-in type -->
<field type="text" name="title" caption="Title" />

<!-- A field class of a plugin -->
<field type="plugin\Orders\Field\ColorField" name="color" caption="Color" />

In a class schema, give the class name:

$fields->add('color')->type(ColorField::class)->caption(__('Color'));

See Class schema.

How a field renders

A field class extends AbstractField, and AbstractField extends Template. Thus each field is a template object with its own template directory.

Where Method of the field Template
Form (insert, edit, info) getEditInput($value, $row). It fires the edit view event and then calls fetchEditInput($value, $row). edit.phtml
List cell getDisplayValue($value, $row) cell_value.phtml
List cell with the attribute trim getDisplayValue($value, $row) cell_value_trim.phtml
Filter fetchFilter() filters/<filter type>.phtml

The method getTemplatePath() returns the template directory of the field. The built-in fields use the directory fields/ of the Theme package:

Field class Template directory
AbstractField (the base) <ENGINE_TEMPLATE_PATH>fields/
SelectField <ENGINE_TEMPLATE_PATH>fields/select/
ForeignKeyField <ENGINE_TEMPLATE_PATH>fields/foreign_key/

Important facts:

  • A field looks in one directory only. It does not use the lookup of the plugin or of the DGS view.
  • Thus a file in the theme or in the plugin does not replace a field template.
  • To use different templates, make a field class and override getTemplatePath() (method 4).

In a field template, $this is the field object.

In the template Content
$this->getName() The field name.
$this->getCaption() The caption.
$this->get('<attribute>') The value of a schema attribute of the field.
$this->getElementAttributes() The HTML attributes from the schema (required, readonly, maxlength, and others) as text.
$this->getCssClassName() The CSS classes of the input.
$this->getStore() The store.
$value In edit.phtml: the value, with HTML characters as entities.
$currentValue In cell_value.phtml: the formatted value.
$rowValues In cell_value.phtml: all values of the row.

Method 1: Cell view handler

The attribute cellViewHandler gives the list cell to a plugin method. See Cell View Handler.

Method 2: Handler field

The type handler gives the input, the list cell, and the value to plugin methods. See Handler Field.

Method 3: Edit view event

The event StoreFieldEvent::EVENT_ON_GET_EDIT_VIEW lets a listener replace the input of one field.

use core\store\event\StoreFieldEvent;

$store = $this->createStoreInstance('orders');

$fields = &$store->getModel()->getFields();

$fields['color']->addEventListener(
    StoreFieldEvent::EVENT_ON_GET_EDIT_VIEW,
    function (StoreFieldEvent &$event) {
        $content = $this->fetch('field_color.phtml', ['value' => $event->getValue()]);

        $event->setContent($content);
    }
);

$store->onRequest($response);

A field of each type fires this event, each time a form draws the field. A listener that gives HTML to setContent() replaces the input. A listener that sets no content can change the value, and the field then draws its own input. See StoreFieldEvent::EVENT_ON_GET_EDIT_VIEW for the methods of the event and for the value that a listener gets.

Caution: The public method getEditInput() of AbstractField fires the event. A field class that overrides getEditInput() does not fire it. Override fetchEditInput() in your field class.

Method 4: Field class

1. Make the class

Put the class in your plugin. Extend AbstractField, or extend a built-in field that is almost correct (for example TextField or SelectField).

plugins/
`-- Orders/
    |-- Field/
    |   `-- ColorField.php
    |-- static/
    |   `-- js/
    |       `-- color_field.js
    `-- templates/
        `-- fields/
            `-- color/
                |-- edit.phtml
                `-- cell_value.phtml
<?php

namespace plugin\Orders\Field;

use AbstractField;
use Core;
use FieldModel;

class ColorField extends AbstractField
{
    public function onInit(FieldModel $scheme): void
    {
        parent::onInit($scheme);

        $plugin = Core::getInstance()->getPluginInstance('Orders');
        $plugin->includeStatic('js/color_field.js');
    }

    protected function getTemplatePath(): string
    {
        return __DIR__.'/../templates/fields/color/';
    }

    public function isValidValue(mixed $value): bool
    {
        if ($value && !preg_match('/^#[0-9a-f]{6}$/i', (string) $value)) {
            $this->setErrorMessage(__('Use the format #rrggbb.'));

            return false;
        }

        return parent::isValidValue($value);
    }
}

What each method does:

Method Use
onInit() Runs when the DGS makes the field. Register the JS and CSS files of the field here.
getTemplatePath() Returns the template directory of the field, with a slash at the end.
isValidValue() Examines the value from the request. Return false and set a message to stop the save.

2. Make the templates

templates/fields/color/edit.phtml:

<?php
/**
 * @var \plugin\Orders\Field\ColorField $this
 * @var string $value
 */
?>
<input type="color"
       class="form-control e-field-color"
       name="<?php echo $this->getName(); ?>"
       data-palette="<?php echo $this->get('palette'); ?>"
       value="<?php echo $value; ?>" <?php echo $this->getElementAttributes(); ?> />

templates/fields/color/cell_value.phtml:

<?php
/**
 * @var \plugin\Orders\Field\ColorField $this
 * @var string $currentValue
 */
?>
<span class="e-color-cell" style="background: <?php echo $currentValue; ?>"><?php echo $currentValue; ?></span>
  • The name attribute of the input must be the field name. The DGS reads the value from the request with this name.
  • A field class that extends AbstractField needs edit.phtml and cell_value.phtml. If the schema uses the attribute trim, it also needs cell_value_trim.phtml.
  • A field class that extends a built-in field can return the directory of the parent for the templates that it does not change.

3. Use the field in a schema

<field type="plugin\Orders\Field\ColorField"
       name="color"
       caption="<?php echo __('Color'); ?>"
       palette="basic"
       isnull="true"
       width="10%" />
  • All general attributes of a field work: caption, required, width, filter, hide, and the others. See Fields.
  • You can add your own attributes (palette in the example). Read them with $this->get('palette').

4. Validation

When isValidValue() returns false, the DGS does not save the row. The browser shows the message of setErrorMessage():

{"messages": ["Use the format #rrggbb."], "storeAction": {}, "type": "alert"}

Always call parent::isValidValue(). The parent examines the attributes required, regexp, and valueFilter.

5. JavaScript and CSS of the field

Register the files in onInit(). See Static Files.

A DGS form opens in a dialog. The browser loads the form with a second request, and that response has no <head>. Thus:

  • The page gets the files when it renders the list. The schema of the list contains the field, so onInit() runs.
  • When the form shows, the inputs are new elements. Start the JavaScript of the field with the event Jimbo.EVENT_ON_DISPLAY_STORE_FORM:
Jimbo.addListener(Jimbo.EVENT_ON_DISPLAY_STORE_FORM, function (event, params) {
    jQuery('.e-field-color').each(function () {
        // Start the component of the field here.
    });
});

If a page shows only the form (not the list), register the files in the plugin method of that page.

6. Other methods that you can override

Method Override it to
fetchEditInput($value, $row) Give more variables to edit.phtml. Call $this->assign() and then $this->fetch('edit.phtml'). The method is protected; getEditInput() calls it when no listener of the edit view event set the content.
getEditInputValue($value) Change the value that the listeners of the edit view event and fetchEditInput() get.
getDisplayValue($value, $row) Change the list cell.
getPreviewValue($value, $row) Change the formatted value that shows outside of an input.
getValueFromRequest($request) Change how the field reads its value from the request (for example, join two inputs).
getFormattedValue($value) Change the format of the value for display.
getQueryWhere() Change the SQL condition of the filter.

A field as a Composer package

Put the field class and its templates into a package when more than one project uses the field. The package MarkdownField is an example:

  • The class extends TextareaField.
  • getBaseTemplatePath() returns the templates directory of the package.
  • onInit() registers the files of the editor with Core::includeJs($url, false) and Core::includeCss($url, false).
  • The schema uses the full class name as the type.

Rules

  • Do not edit the field templates in vendor/.
  • Do not add a field class to the Core. Put it in a plugin or in a package.
  • Use the full class name as the type. A short type name works only for a class without a namespace.
  • Keep the logic in the field class and the HTML in the templates.
  • Do not write <script> blocks with logic in a field template. Put the logic in a static file.
  • Use __() for each text that the user sees.