Using [[+rows]] from repeater multiple times in the outer template, but with different chunks

Hi there,

I am using Content Blocks 1.4.1.
As mentioned in the topic, I would like to use the [[+rows]] placeholder multiple times in my outer template chunk, but with a different chunk each time. The use case would be a tab navigation. I need to iterate over the rows more than once - the first time to build the ul li elements for the navigation, second time for the content itself.

I tried to use a snippet that sets a placeholder and increases each time, but that was cached after the first repeater row with the last value. Any idea? :slight_smile:

One way I’ve solved that particular problem is to have the template output JSON data, then have a snippet that I run in the wrapper template that takes that JSON data as input.

I actually have the snippet’s return value be basically a chunk call that’s built, because that chunk call is written into the [[*content]] field and is parsed as if it were a normal chunk call directly written into the [[*content]].

That way, updating the chunks doesn’t mean you have to rebuild the content (if the snippet returned HTML instead of a chunk call, you’d have to rebuild the content each time you update your chunks, because it’s already parsed by the time it gets into [[*content]]).

thanks Isaac for this solution!

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>

2 Likes

How is it possible to do the same with 3 Rows?

Shoud by quite easy, just modify /core/components/mxCustomCB/elements/inputs/repeaterinput2.class.php from my example like this (not tested):

<?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><div>[[+rows3]]</div>';
    
    public function getName()
    {
        return 'Repeater2'; 
    }
    
    public function getDescription()
    {
        return 'Repeater with 3 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' => 'template3',
                'fieldLabel' => 'Tertiary 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' => 'row_separator3',
                'fieldLabel' => 'Tertiary 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');
        $tpl3 = $field->get('template3');

        // Array to store the output in
        $rowsOutput = array();
        $rowsOutput2 = array();
        $rowsOutput3 = 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);
            $rowsOutput3[] = $this->processRow($row, $group, $data, $tpl3);
        }

        $data['total'] = count($data['rows']);

        // Glue individual rows together
        $separator = $field->get('row_separator');
        $separator2 = $field->get('row_separator2');
        $separator3 = $field->get('row_separator3');
        if (empty($separator)) $separator = "\n\n";
        if (empty($separator2)) $separator2 = "\n\n";
        if (empty($separator3)) $separator3 = "\n\n";
        $rowsOutput = implode($separator, $rowsOutput);
        $rowsOutput2 = implode($separator2, $rowsOutput2);
        $rowsOutput3 = implode($separator2, $rowsOutput3);

        // Throw it in a wrapper template with [[+rows]]
        $wrapperTpl = $field->get('wrapper_template');
        if (empty($wrapperTpl)) $wrapperTpl = '[[+rows]]';
        $data['rows'] = $rowsOutput;
        $data['rows2'] = $rowsOutput2;
        $data['rows3'] = $rowsOutput3;

        // 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;
    }
}