Recently we ran into the same problem with advanced tab bootstrap component. After examining more options I have decided to create custom input type, cloning Repeater input. Aside from a little confusion with registering js files/classes and plugins, it takes only a few lines of changed code. I guess folks at modmore could easily build something similar into next ContentBlock release.
This is my solution if somebody needs it:
The only file that needs to be really changed is inputtype class itself, to add input field properties and change render logic. Due to way input types are handled you also need a copy of js file, where the only change is input type idetificator. You can use original Repeater templates, no need to copy them, there is no change, only need to wrap them in your inputtype idetificator for proper registration.
I have not messed up with config values or lexicon entries, all directory names are hardcoded. I have cloned directory structure of original ContentBlock files, but used “mxCustomCB” as component name in directory structure.
File /core/components/mxCustomCB/elements/inputs/repeaterinput2.class.php:
<?php
/**
* Class RepeaterInput2
*
* Repeats groups of fields in different rows. Nifty stuff.
*/
class RepeaterInput2 extends cbBaseInput {
public $defaultIcon = 'chunk_A';
public $defaultTpl = ' ';
public $defaultWrapperTpl = '<div>[[+rows]]</div><div>[[+rows2]]</div>';
public function getName()
{
return 'Repeater2';
}
public function getDescription()
{
return 'Repeater with 2 separate templates';
}
/**
* @return array
*/
public function getFieldProperties()
{
return array(
array(
'key' => 'template2',
'fieldLabel' => 'Secondary template',
'xtype' => 'code',
'default' => $this->defaultTpl,
'description' => $this->modx->lexicon('contentblocks.repeater.wrapper_template.description')
),
array(
'key' => 'wrapper_template',
'fieldLabel' => $this->modx->lexicon('contentblocks.wrapper_template'),
'xtype' => 'code',
'default' => $this->defaultWrapperTpl,
'description' => $this->modx->lexicon('contentblocks.repeater.wrapper_template.description')
),
array(
'key' => 'group',
'fieldLabel' => $this->modx->lexicon('contentblocks.repeater.group'),
'xtype' => 'fieldgroup', // special type which creates a grid of fields under the current field
'description' => $this->modx->lexicon('contentblocks.repeater.group.description')
),
array(
'key' => 'row_separator',
'fieldLabel' => $this->modx->lexicon('contentblocks.repeater.row_separator'),
'xtype' => 'textfield',
'description' => $this->modx->lexicon('contentblocks.repeater.row_separator.description'),
'default' => "\n\n"
),
array(
'key' => 'row_separator2',
'fieldLabel' => 'Secondary row separator',
'xtype' => 'textfield',
'description' => $this->modx->lexicon('contentblocks.repeater.row_separator.description'),
'default' => "\n\n"
),
array(
'key' => 'max_items',
'fieldLabel' => $this->modx->lexicon('contentblocks.repeater.max_items'),
'xtype' => 'numberfield',
'description' => $this->modx->lexicon('contentblocks.repeater.max_items.description'),
'default' => 0,
'minValue' => 0
),
array(
'key' => 'min_items',
'fieldLabel' => $this->modx->lexicon('contentblocks.repeater.min_items'),
'xtype' => 'numberfield',
'description' => $this->modx->lexicon('contentblocks.repeater.min_items.description'),
'default' => 0,
'minValue' => 0
),
array(
'key' => 'add_first_item',
'fieldLabel' => $this->modx->lexicon('contentblocks.repeater.add_first_item'),
'xtype' => 'contentblocks-combo-boolean',
'description' => $this->modx->lexicon('contentblocks.repeater.add_first_item.description'),
'default' => true,
),
);
}
/**
* Similar to {@see self::getFieldProperties}, except this is used when creating subfields to modify the edit panel.
*
* @return array
*/
public function getParentProperties()
{
return array(
array(
'key' => 'key',
'fieldLabel' => $this->modx->lexicon('contentblocks.repeater.key'),
'xtype' => 'textfield',
'default' => '',
'description' => $this->modx->lexicon('contentblocks.repeater.key.description'),
'allowBlank' => false
),
array(
'key' => 'width',
'fieldLabel' => $this->modx->lexicon('contentblocks.width'),
'xtype' => 'numberfield',
'description' => $this->modx->lexicon('contentblocks.width.description'),
'allowBlank' => false
),
);
}
/**
* Returns an array of javascript files to load.
*
* @return array
*/
public function getJavaScripts()
{
return array(
MODX_ASSETS_URL . 'components/mxCustomCB/js/inputs/repeater2.js',
);
}
/**
* Load the template for the input
*
* @return array
*/
public function getTemplates()
{
$tpls = array();
//$tpls[] = $this->contentBlocks->getCoreInputTpl('repeater');
$corePath = $this->modx->getOption('contentBlocks.core_path', null, MODX_CORE_PATH . 'components/contentblocks/');
$template = file_get_contents($corePath . 'templates/inputs/repeater.tpl');
$tpls[] = $this->contentBlocks->wrapInputTpl('repeater2', $template);
$tpls[] = $this->contentBlocks->getCoreTpl('inputs/partials/repeater_item', 'contentblocks-repeater-item');
return $tpls;
}
/**
* Generate the HTML for the repeater
*
* @param cbField $field
* @param array $data
* @return mixed
*/
public function process(cbField $field, array $data = array())
{
// Ensure inputs are loaded
//$this->contentBlocks->loadInputs();
// Grab the group fields and template
$group = $this->getGroup($field);
$tpl = $field->get('template');
$tpl2 = $field->get('template2');
// Array to store the output in
$rowsOutput = array();
$rowsOutput2 = array();
// Loop over each row
$idx = 0;
foreach ($data['rows'] as $row) {
$idx++;
$data['idx'] = $idx;
$rowsOutput[] = $this->processRow($row, $group, $data, $tpl);
$rowsOutput2[] = $this->processRow($row, $group, $data, $tpl2);
}
$data['total'] = count($data['rows']);
// Glue individual rows together
$separator = $field->get('row_separator');
$separator2 = $field->get('row_separator2');
if (empty($separator)) $separator = "\n\n";
if (empty($separator2)) $separator2 = "\n\n";
$rowsOutput = implode($separator, $rowsOutput);
$rowsOutput2 = implode($separator2, $rowsOutput2);
// Throw it in a wrapper template with [[+rows]]
$wrapperTpl = $field->get('wrapper_template');
if (empty($wrapperTpl)) $wrapperTpl = '[[+rows]]';
$data['rows'] = $rowsOutput;
$data['rows2'] = $rowsOutput2;
// Return the final output. Whew.
return $this->contentBlocks->parse($wrapperTpl, $data);
}
/**
* Processes a single row of the repeater
*
* @param array $row
* @param cbField[] $group
* @param array $data
* @param string $tpl
* @return mixed
*/
public function processRow($row, $group, $data, $tpl = '') {
// For each row, we store placeholders in the $rowFields array
$rowFields = array();
// Loop over each key in the row and its value (array)
foreach ($row as $key => $value) {
$field = array_key_exists($key, $group) ? $group[$key] : false;
if ($field instanceof cbField) {
$inputType = $field->get('input');
// If it's a known input, we try to parse it
if (isset($this->contentBlocks->inputs[$inputType])) {
/** @var cbBaseInput $input */
$input = $this->contentBlocks->inputs[$inputType];
// Attempt to parse the data through that input type
try {
$parseData = array_merge($data, $field->toArray(), $value);
$value = $input->process($field, $parseData);
} catch (Exception $e) {
$value = 'Error parsing ' . $inputType . ': ' . $e->getMessage();
}
} else {
$value = 'Input ' . htmlentities($inputType, ENT_QUOTES, 'UTF-8') . ' not found.';
}
}
else {
$value = 'Could not find subfield with key "' . $key . '" in the group"';
}
// Set the value as placeholder in $rowFields
$rowFields[$key] = $value;
}
// Grab the $data and the $rowFields together so we have settings and everything
$phs = array_merge($data, $rowFields);
// Parse this row of fields
return $this->contentBlocks->parse($tpl, $phs);
}
/**
* Gets the repeater sub fields as key => cbField array
*
* @param cbField $field
* @return cbField[]
*/
public function getGroup(cbField $field)
{
$group = array();
$fields = $field->getSubfields();
foreach ($fields as $fld) {
$key = $fld->getParentProperty('key');
if (!empty($key)) {
$group[$key] = $fld;
}
}
return $group;
}
/**
* Return an array of input keys that need to be loaded whenever
* this input is being used.
*
* Contains a reference to the field it is being used on in case
* it depends on configuration.
*
* @param cbField $field
* @return array
*/
public function getDependantInputs(cbField $field) {
$group = $this->getGroup($field);
$dependencies = array();
foreach ($group as $subField) {
$dependencies[] = $subField->get('input');
if ($subField->get('input') === 'repeater') {
$nestedGroup = $this->getGroup($subField);
foreach ($nestedGroup as $nestedField) {
$dependencies[] = $nestedField->get('input');
}
}
}
return $dependencies;
}
}
Copy /assets/components/contentblocks/js/inputs/repeater.js into /assets/components/mxCustomCB/js/inputs/repeater2.js and change second line:
ContentBlocks.fieldTypes.repeater2 = function(dom, data) {
Create plugin (name doesn’t matter and register with ContentBlocks_RegisterInputs:
<?php
/**
* @var modX $modx
* @var ContentBlocks $contentBlocks
* @var array $scriptProperties
*/
if ($modx->event->name == 'ContentBlocks_RegisterInputs') {
// Load your own class. No need to require cbBaseInput, that's already loaded.
$path = MODX_CORE_PATH . 'components/mxCustomCB/';
require_once($path . 'elements/inputs/repeaterinput2.class.php');
// Create an instance of your input type, passing the $contentBlocks var
$instance = new RepeaterInput2($contentBlocks);
// Pass back your input reference as key, and the instance as value
$modx->event->output(array(
'repeater2' => $instance
));
}
Don’t forget to clear cache. Create new field using Repeater2 input type. In field properties you will notice new “Secondary template” and “Secondary separator” fields. Also, default wrapper template is someting like
<div>[[+rows]]</div><div>[[+rows2]]</div>