In contrast to the RepeaterMatrix, inputfields for items used in RepeaterFlex need to be declared with an associative array. Each key specifies the name of a field, the associated value define additional attributes. Each RepeaterFlex item plugin must implement a static method getFields() to advertize the requested fields. If no fields are needed, implementing this method may be omitted, though.
Fields may be specified in two flavors. The simpler form just use the name of an already existing field, if no adjustments need to be done, the value array may be left empty. Such a minimal, yet complete, specification may look like this:
public static function getFields() { return [
// Refer to an existing field, use it as is.
'email' => [ ],
];
}
Please note that using the system field email really is a bad example in this case. You should avoid adding this field to a repeater, since it will be preloaded (along with some bound system fields) with each repeater item, this slows down repeater enumeration significantly (also for regular Repeater and RepeaterMatrix).
Let's take a look at the value array next, which defines additional information for this field. For now we'll look at the key 'context' first, which allows to overwrite nearly any aspect of an existing field, similar to what you do when adjusting field attributes in your template definition. The most interesting attributes are probably 'label' and 'columnWidth', like we do in our next example:
// Refer to an existing field, but change some attributes
'roles' => [
'context' => [
'label' => __("Select allowed roles"),
'description' => __("Select roles allowed to use this field, others will see an error message!"),
'columnWidth' => 25,
],
],
Please note that the roles field is flagged permanent. Once added to the repeater's template, it can not be removed. While this is totally uncritical, you'll just get a warning when removing a plugin using this field from the RepeaterFlex.
This already is a pretty powerful approach, which allows re-use most fieldtypes and tweak their attributes to match your needs. If you prefer, you may create any required field the usual way first and keep the definition simply as these. But if you intend to migrate a plugin into a second installation, you'll have to create the field first so the plugin may refer to it. You may use the JSON Field-Exporter/-Importer of ProcessWire to simplify field migration a little. But be aware this does not work with option fields, may fail due to duplicated field ids and maybe more.
{ "header_type": { "id": 152, "type": "FieldtypeOptions", "flags": 0, "name": "header_type", "label": "Paragraph Type", "inputfieldClass": "InputfieldSelect", "required": 1, "defaultValue": "p", "columnWidth": 25, "initValue": "", "collapsed": 0, "showIf": "", "themeInputSize": "", "themeInputWidth": "", "themeOffset": "", "themeBorder": "", "themeColor": "", "requiredIf": "", "export_options": { "default": "1=p|Standard\n2=h1|Header1\n3=h2|Header2\n4=h3|Header3\n5=h4|Header4\n6=h5|Header5" } } }
So, why not just take the exported script and let RepeaterFlex do the import for you? This is the responsibility of the 'setup' key, which looks rather similar to an exported script, but you'll need to translate the JSON format into a PHP array, so replace colons with '=>' and curly braces with brackets. Additionally you want to remove entries 'id', 'flags', 'name' and probably all defaults (set to 0 or "") and if its an Options field you'll need to transcode the "export_options" into an associative array as well. In the end this may look like this:
// Refer to a real field, create if it does not exist
'header_type' => [
'context' => [
'columnWidth' => 25,
],
'setup' => [ // may be used to create fields automatically
'type' => 'FieldtypeOptions',
'label' => __('Paragraph Type'),
'inputfieldClass' => 'InputfieldSelect',
'required' => 1,
'defaultValue' => 'p',
'initValue' => '',
'columnWidth' => 25,
'options' => [
1 => 'p|'.__('Standard'),
2 => 'h1|'.__('Header1'),
3 => 'h2|'.__('Header2'),
4 => 'h3|'.__('Header3'),
5 => 'h4|'.__('Header4'),
6 => 'h5|'.__('Header5'),
],
],
],
When you add a plugin with such a definition to your RepeaterFlex field, then the field 'header_type' will be created on the fly, which simplifies re-use of plugins significantly.
Of course you still may run into issues, if the system already has a field with the same name but totally different use. Then you either need to modify the script or rename your system field, so there still is room for improvement.
With the availability to teak field attributes using the 'context' array, why not generally refer only to existing fieldtypes but using a fixed name in your script?
Let's say, you need a simple, non-markup textfield which probably exists in any PW installation. What if we simply describe our requirements and let the user pick a field when configuring the RepeaterFlex field. That's what I call a virtual field.
Such a virtual field is identified using the prefix @ before the name and the existence of keys 'hint' and 'fieldtype' in its configuration. Something like this:
// Define a textfield for markup using field Prototypes in RepeaterFlexItem, specify label only
'@TextOnly' => [
'fieldtype' => 'Text',
'hint' => __('Select a plain Textfield without markup'),
'context' => [
'label' => __('Info for restricted roles'),
'required' => 1,
'columnWidth' => 25,
],
],
The 'fieldtype' specification is used to filter selectable fields during configuration, the 'hint' is displayed next to the selection, which we'll see soon. Inside the render method this field is referenced using $pg->TextOnly, regardless how it is named in the system.
Please note that for technical reasons the first character is responsible to decide if name mapping actually takes place or not. Fieldnames starting with an uppercase letter will be translated, according to the mapping, lowercase names refer to native fields.
While all these methods work with nearly any field, tweaking attributes will fail with any kind of Options field, since options are finally managed from the database, so redefinition of options during runtime is not possible. For this reason the RepeaterFlex comes with a very special fieldtype named FieldtypeOptionsFlex. This fieldtype is tightly bound to the RepeaterFlex, since it pulls the options from the plugin directly which allow re-configuration during runtime and therefore re-use of such fields. In comparison to the TextOnly field we basically add an 'options' key:
// Define an option field using field Prototypes in RepeaterFlexItem
'@ColorOption' => RepeaterFlexItem::Options([
'columnWidth' => 25,
'label' => __("Message color"),
'inputfieldClass' => 'InputfieldSelect',
'required' => 1,
'defaultValue' => '1',
'columnWidth' => 25,
'options' => [
1 => [ 'color' => '#F00', 'title' => __('red') ],
2 => [ 'color' => '#0F0', 'title' => __('green') ],
3 => [ 'color' => '#00F', 'title' => __('blue') ],
4 => [ 'color' => '#000', 'title' => __('black') ],
],
]),
We need to focus on three points here, let's start with the 'options' array. In constrast to the options definition for the 'header_type' above, the values are associative arrays again. While a 'title' field is mandatory per entry, you are totally free to pack whatever you need into each option. To retrieve currently selected color and title, just use $pg->ColorOption->color and $pg->ColorOption->title, respectively. If you only need 'value' and 'title', you may stay with the string argument.
The second notable point is 'inputfieldClass', which specifies the kind of input control. You may use InputfieldSelect and InputfieldRadios for single selection and InputfieldCheckboxes, InputfieldMultiSelect and InputfieldAsmSelect for multi-selections. In contrast to "real" Options fields, the FieldtypeOptionsFlex allows this configuration during runtime and only during runtime.
And finally, the most interesting difference to our TextOnly field is the lack of a 'context' key and call to a static method RepeaterFlexItem::Options from the baseclass. There are several similar methods for most common fieldtypes which only take a single context array and return the complete field specification. So you do not copy&paste 'hint' and 'fieldtype' for each new field:
RepeaterFlexItem::TextMarkup($opt=null); // Single Textline with Markup
RepeaterFlexItem::TextAreaMarkup($opt=null) // Textline which returns Markup
RepeaterFlexItem::TextOnly($opt=null) // Plain Textline without Markup (i.e. options, classes, configuration)
RepeaterFlexItem::TextArea($opt=null) // Plain TextArea
RepeaterFlexItem::Url($opt=null) // A field for a URL
RepeaterFlexItem::Image($opt=null) // A field for a single image
RepeaterFlexItem::Images($opt=null) // A field for multiple images
RepeaterFlexItem::Options($opt=null) // A field of FieldtypeOptionsFlex
RepeaterFlexItem::PageSelect($opt=null) // A Page Selector
RepeaterFlexItem::Checkbox($opt=null) // A single Checkbox
RepeaterFlexItem::Percent($opt=null) // A numerical percentage value
The $opt parameter may be a regular 'context' array, a string, which would define the label or an integer to specify the columnWidth. So a probably most simple virtual field definition may look like this:
// Define a virtual field using field prototype from RepeaterFlexItem and specify only the title.
'@FancyImages' => RepeaterFlexItem::Images(__('Upload only nice pictures!')),
When using this plugin the first time, the configuration will detect missing fields, so you'll see something like this:
Submitting the form will not only install the header_type field:
but also opens two new assignment fields for FancyImages and ColorOption:
Please note that the system actually didn't query for the TextOnly field, since this already was assigned as it is used from multiple other fields.
By the way, if you ever want to know the mapping for a particular plugin, checkout the verbose field on top of the RepeaterFlex configuration. Our new FlexFieldsDemo state this:
Now, that we added and configured this field to our RepeaterMatrix successfully, let's do some input:
And finally have a quick look at the render method, so you get an idea what this field actually should do:
<?php namespace ProcessWire;
class FlexFieldsDemo extends RepeaterFlexItem {
public static function getModuleInfo() {
return array(
'title' => __('Flex Fields Demo', __FILE__), // Module Title
'summary' => __('Example on how to specify input fields for an item.', __FILE__), // Module Summary
'version' => 1,
'renderer' => 'ContentRenderer',
'head' => '{flex_label} [• {FancyImages.count} Images, Roles: {roles*name}, {TextOnly}, {header_type}/{ColorOption.title} ]',
'icon' => 'fa fa-thumbs-o-up',
);
}
public static function getFields() { return [
// Refer to an existing field, use it as is.
// Avoid using the standard email field on a productive system
// since it slows down repeater enumeration significantly (the field.
// is retrieved for each repeater along with some hooked system stuff).
// This is only used as a very simple example!
// 'email' => [ ],
// Refer to an existing field, but change some attributes
// Please note that the regular roles field is marked permanent so
// that it can not be removed from the repeater template afterwards.
'roles' => [
'context' => [
'label' => __("Select allowed roles"),
'description' => __("Select roles allowed to use this field, others will see an error message!"),
'columnWidth' => 25,
],
],
// Refer to a real field, create if it does not exist
'header_type' => [
'context' => [
'columnWidth' => 25,
],
'setup' => [ // may be used to create fields automatically
'type' => 'FieldtypeOptions',
'label' => __('Paragraph Type'),
'inputfieldClass' => 'InputfieldSelect',
'required' => 1,
'defaultValue' => 'p',
'initValue' => '',
'columnWidth' => 25,
'options' => [
1 => 'p|'.__('Standard'),
2 => 'h1|'.__('Header1'),
3 => 'h2|'.__('Header2'),
4 => 'h3|'.__('Header3'),
5 => 'h4|'.__('Header4'),
6 => 'h5|'.__('Header5'),
],
],
],
// Define a textfield for markup using field Prototypes in RepeaterFlexItem, specify label only
'@TextOnly' => [
'fieldtype' => 'Text',
'hint' => __('Select a plain Textfield without markup'),
'context' => [
'label' => __('Info for restricted roles'),
'required' => 1,
'columnWidth' => 25,
],
],
// Define an option field using field Prototypes in RepeaterFlexItem
'@ColorOption' => RepeaterFlexItem::Options([
'columnWidth' => 25,
'label' => __("Message color"),
'inputfieldClass' => 'InputfieldSelect',
'required' => 1,
'defaultValue' => '1',
'columnWidth' => 25,
'options' => [
1 => [ 'color' => '#F00', 'title' => __('red') ],
2 => [ 'color' => '#0F0', 'title' => __('green') ],
3 => [ 'color' => '#00F', 'title' => __('blue') ],
4 => [ 'color' => '#000', 'title' => __('black') ],
],
]),
// Define a virtual field using field prototype from RepeaterFlexItem and specify only the title.
'@FancyImages' => RepeaterFlexItem::Images(__('Upload only nice pictures!')),
];
}
public function render(Page $pg) {
$user = wire('user'); // Obtain current user
$bOk = false;
foreach($pg->roles as $role) // Enumerate selected roles...
{
if($user->hasRole($role)) // ...and check if the current user has at least one of them.
$bOk = true;
}
if(!$bOk) // User does not have any of the allowed roles, output the configured warning.
{
$Tag = $this->ctx->getTag($pg->header_type->value);
$Tag->addAttributes([ 'style' => "color:{$pg->ColorOption->color};" ]);
return($Tag->getOpenTag() . $pg->TextOnly . $Tag->getCloseTag());
}
// User has at least one allowed role, output images as thumbnails
$out = '<div>';
foreach($pg->FancyImages as $img)
$out .= '<img src="'.$img->size(300,300)->url.'"/>';
$out .= '</div>';
return($out);
}
}
public function render(Page $pg) {
$user = wire('user'); // Obtain current user
$bOk = false;
foreach($pg->roles as $role) // Enumerate selected roles...
{
if($user->hasRole($role)) // ...and check if the current user has at least one of them.
$bOk = true;
}
if(!$bOk) // User does not have any of the allowed roles, output the configured warning.
{
$Tag = $this->ctx->getTag($pg->header_type->value);
$Tag->addAttributes([ 'style' => "color:{$pg->ColorOption->color};" ]);
return($Tag->getOpenTag() . $pg->TextOnly . $Tag->getCloseTag());
}
// User has at least one allowed role, output images as thumbnails
$out = '<div>';
foreach($pg->FancyImages as $img)
$out .= '<img src="'.$img->size(300,300)->url.'"/>';
$out .= '</div>';
return($out);
}
I guess, there is little to explain. In case you are a logged in user, you'll see this a second time below: