src/Form/Control/DropdownCascade.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

declare(strict_types=1);

namespace Atk4\Ui\Form\Control;

use Atk4\Data\Model;
use Atk4\Ui\Form;
use Atk4\Ui\Js\Jquery;
use Atk4\Ui\Js\JsBlock;

/**
 * Dropdown form control that will based it's list value
 * according to another input value.
 * Also possible to cascade value from another cascade field.
 */
class DropdownCascade extends Dropdown
{
    /** @var string|Form\Control The form control to use for setting this dropdown list values from. */
    public $cascadeFrom;

    /** @var string|Model|null The hasMany reference model that will generate value for this dropdown list. */
    public $reference;

    #[\Override]
    protected function init(): void
    {
        parent::init();

        if (!$this->cascadeFrom instanceof Form\Control) {
            $this->cascadeFrom = $this->form->getControl($this->cascadeFrom);
        }

        $cascadeFromValue = $this->getApp()->hasRequestPostParam($this->cascadeFrom->name)
            ? $this->getApp()->uiPersistence->typecastAttributeLoadField($this->cascadeFrom->entityField->getField(), $this->getApp()->getRequestPostParam($this->cascadeFrom->name))
            : $this->cascadeFrom->entityField->get();

        $this->model = $this->cascadeFrom->model ? $this->cascadeFrom->model->ref($this->reference) : null;

        // populate default dropdown values
        $this->dropdownOptions['values'] = $this->getJsValues($this->getNewValues($cascadeFromValue), $this->entityField->get());

        // JS to execute for the onChange handler of the parent dropdown
        $expr = [
            function (Jquery $j) use ($cascadeFromValue) {
                return new JsBlock([
                    $this->jsDropdown()->dropdown('change values', $this->getNewValues($cascadeFromValue)),
                    $this->jsDropdown()->removeClass('loading'),
                ]);
            },
            $this->jsDropdown()->dropdown('clear'),
            $this->jsDropdown()->addClass('loading'),
        ];

        $this->cascadeFrom->onChange($expr, ['args' => [$this->cascadeFrom->name => $this->cascadeFrom->jsInput()->val()]]);
    }

    #[\Override]
    public function set($value = null)
    {
        $this->dropdownOptions['values'] = $this->getJsValues($this->getNewValues($this->cascadeFrom->entityField->get()), $value);

        return parent::set($value);
    }

    /**
     * Generate new dropdown values based on cascadeInput model selected ID.
     * Return an empty value set if ID is null.
     *
     * @param mixed $id
     *
     * @return list<array{value: string, text: mixed, name: mixed}>
     */
    public function getNewValues($id): array
    {
        if ($id === null) {
            return [[
                'value' => '',
                'text' => $this->empty,
                'name' => $this->empty,
            ]];
        }

        $model = $this->cascadeFrom->model->load($id)->ref($this->reference);
        $values = [];
        foreach ($model as $row) {
            if ($this->renderRowFunction) {
                $res = ($this->renderRowFunction)($row);
                $values[] = [
                    'value' => $this->getApp()->uiPersistence->typecastAttributeSaveField($model->getIdField(), $row->getId()),
                    'text' => $res['title'],
                    'name' => $res['title'],
                ];
            } else {
                $values[] = [
                    'value' => $this->getApp()->uiPersistence->typecastAttributeSaveField($model->getIdField(), $row->getId()),
                    'text' => $row->get($model->titleField),
                    'name' => $row->get($model->titleField),
                ];
            }
        }

        return $values;
    }

    /**
     * Will mark current value as selected from a list
     * of possible values.
     *
     * @param list<array{value: string, text: mixed, name: mixed}> $values
     * @param mixed                                                $value  the current field value
     *
     * @return list<array{value: string, text: mixed, name: mixed, selected: true}>
     */
    private function getJsValues(array $values, $value): array
    {
        $model = $this->cascadeFrom->model->ref($this->reference);
        $valueStr = $this->getApp()->uiPersistence->typecastAttributeSaveField($model->getIdField(), $value);

        foreach ($values as $k => $v) {
            if ($v['value'] === $valueStr) {
                $values[$k]['selected'] = true;

                break;
            }
        }

        return $values;
    }

    #[\Override]
    protected function htmlRenderValue(): void
    {
        // called in parent::renderView(), but values are rendered only via JS
    }

    #[\Override]
    protected function renderView(): void
    {
        // multiple selection is not supported
        $this->multiple = false;

        parent::renderView();
    }
}