XF 2.1 Calling \XF\Option\Forum::renderSelectMultiple from a template

TiKu

New member
Hi,

For my add-on I would like to create an option that consists of a checkbox and a dependent forum (multi-)selection box. The forum selection box can be created with a call to \XF\Option\Forum::renderSelectMultiple. I know that I can create a simple forum selection option by setting its Edit format to PHP callback, its Format parameters to \XF\Option\Forum::renderSelectMultiple, its Data type to Array, and its Array sub-options to *. But how do I do this in a complex option, that uses the Edit format Named template in order to display the forum selector only if a checkbox has been checked?

I tried using xf:callback with params="[{$option}]", but no matter how I try to pass $option, I always get an error telling me that a string instead of a \XF\Entity\Option has been passed to renderSelectMultiple.
Am I trying something impossible?

Regards
TiKu
 

Jeremy P

Well-known member
I would consider creating your own option callback (adapting it from \XF\Option\Forum). Callbacks support rendering a template (see \XF\Option\AbstractOption::getTemplate()), so you can use them to fetch the data you need and render your option template. In your template just render the forum select row manually (via <xf:selectrow>) as a dependent of the checkbox.
 

TiKu

New member
I'm not sure I understood correctly. I should implement a class that derives from AbstractOption and has a function similar to renderSelectMultiple and my option should point to this class' function as a PHP callback, right?
I also should create a template that combines the checkbox and the select and is used by my render function, right?
I think my problem is that I don't understand how my template must look like. It seems like renderSelectMultiple does not use a template at all, but XF\Template\Templater::formSelect (which is called by XF\Template\Templater::formSelect, which is called by renderSelectMultiple) creates HTML directly.
So to me it seems like using a template won't work in this case. But without template I don't know how to do the xf:dependent stuff.
 

TiKu

New member
I got it working to some degree. The option behaves as expected, but it cannot save its values. Here is my implementation:

XF\Option\Forum.php:
Code:
<?php

namespace My\AddOn\XF\Option;

use XF\Option\AbstractOption;
use XF\Mvc\Entity\Entity;

class Forum extends AbstractOption
{
    public static function renderSelect(\XF\Entity\Option $option, array $htmlParams)
    {
        $data = self::getSelectData($option, $htmlParams);

        return self::formSelectRowWithCheckBox(
            $option, $htmlParams, $data['controlOptions'], $data['choices'], $data['rowOptions']
        );
    }

    public static function renderSelectMultiple(\XF\Entity\Option $option, array $htmlParams)
    {
        $data = self::getSelectData($option, $htmlParams);
        $data['controlOptions']['multiple'] = true;
        $data['controlOptions']['size'] = 8;

        return self::formSelectRowWithCheckBox(
            $option, $htmlParams, $data['controlOptions'], $data['choices'], $data['rowOptions']
        );
    }

    protected static function getSelectData(\XF\Entity\Option $option, array $htmlParams)
    {
        /** @var \XF\Repository\Node $nodeRepo */
        $nodeRepo = \XF::repository('XF:Node');

        $choices = $nodeRepo->getNodeOptionsData(true, 'Forum', 'option');
        $choices = array_map(function($v) {
            $v['label'] = \XF::escapeString($v['label']);
            return $v;
        }, $choices);

        $ret = [
            'choices' => $choices,
            'controlOptions' => self::getControlOptions($option, $htmlParams),
            'rowOptions' => self::getRowOptions($option, $htmlParams)
        ];

        $selectKey = array_keys($option->default_value)[1];
        $ret['controlOptions']['name'] = $htmlParams['inputName'] . '[' . $selectKey . ']';
        return $ret;
    }

    protected static function formSelectRowWithCheckBox(\XF\Entity\Option $option, array $htmlParams, array $selectControlOptions, array $selectChoices, array $rowOptions)
    {
        $templater = self::getTemplater();
        self::addToClassAttribute($rowOptions, 'formRow--input', 'rowclass');

        $selectControlId = self::assignFormControlId($selectControlOptions);

        $checkBoxKey = array_keys($option->default_value)[0];
        $checkBoxName = $htmlParams['inputName'] . '[' . $checkBoxKey . ']';
        $checkBoxChoices = [
            [
                'name' => $checkBoxName,
                'label' => \XF::phrase('option.myAddOnRestrictToForums'),
                'value' => \XF::app()->getContentTypePhrase(Entity::BOOL),
                '_dependent' => [
                    $templater->formSelect($selectControlOptions, $selectChoices)
                ]
            ]
        ];

        $checkBoxValue = '0';
        if(in_array($checkBoxKey, array_keys($option->option_value)))
        {
            $checkBoxValue = $option->option_value[$checkBoxKey] ? \XF::app()->getContentTypePhrase(Entity::BOOL) : '0';
        }
        else
        {
            $checkBoxValue = $option->default_value[$checkBoxKey] ? \XF::app()->getContentTypePhrase(Entity::BOOL) : '0';
        }

        $checkBoxControlOptions = [
            //'name' => '',
            'value' => $checkBoxValue
        ];
        $optionHtml = $templater->formCheckBox($checkBoxControlOptions, $checkBoxChoices);

        return $optionHtml ? $templater->formRow($optionHtml, $rowOptions, $selectControlId) : '';
    }

    protected static function addToClassAttribute(array &$options, $class, $key = 'class')
    {
        if(!isset($options[$key]))
        {
            $options[$key] = '';
        }

        if (strlen($options[$key]))
        {
            $options[$key] .= " $class";
        }
        else
        {
            $options[$key] = $class;
        }
    }

    protected static function assignFormControlId(array &$controlOptions)
    {
        if (!empty($controlOptions['id']))
        {
            return $controlOptions['id'];
        }

        $controlOptions['id'] = self::getTemplater()->fn('unique_id');
        return $controlOptions['id'];
    }
}
My option config:
  • Edit format: PHP callback
  • Format parameters: \My\AddOn\XF\Option\Forum::renderSelectMultiple
  • Data type: Array
  • Default value: {"doRestrictToForums":"0","restrictToForums":[]}
  • Array sub-options:
    Code:
    doRestrictToForums
    *
The option consists of a checkbox and a forum selection box, exactly how I want it. The forum selector is disabled if the checkbox is not checked, exactly how I want it. But hitting the save button always will write this option_value to the database:
{"doRestrictToForums":false}

What is missing?
 

TiKu

New member
Okay, it is working now. I had to adjust $controlOptions['value'] in getSelectData:
Code:
        if(in_array($selectKey, array_keys($option->option_value)))
        {
            $ret['controlOptions']['value'] = $option->option_value[$selectKey] ? $option->option_value[$selectKey] : $option->default_value[$selectKey];
        }
        else
        {
            $ret['controlOptions']['value'] = $option->default_value[$selectKey];
        }
Overall, I feel that my implementation is not the cleanest one, so improvements are welcome. But it works.
 
Top