src/JsCallback.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

declare(strict_types=1);

namespace Atk4\Ui;

use Atk4\Ui\Js\Jquery;
use Atk4\Ui\Js\JsBlock;
use Atk4\Ui\Js\JsExpression;
use Atk4\Ui\Js\JsExpressionable;

class JsCallback extends Callback
{
    /** @var array<string, string|JsExpressionable> Holds information about arguments passed in to the callback. */
    public $args = [];

    /** @var string Text to display as a confirmation. Set with setConfirm(..). */
    public $confirm;

    /** @var array<string, mixed> Use this apiConfig variable to pass API settings to Fomantic-UI in .api(). */
    public array $apiConfig = [];

    /** @var string|null Include web storage data item (key) value to be included in the request. */
    public $storeName;

    /**
     * Usually JsCallback should not allow to trigger during a reload.
     * Consider reloading a form, if triggering is allowed during the reload process
     * then $form->entity could be saved during that reload which can lead to unexpected result
     * if model ID is not properly handled.
     *
     * @var bool
     */
    public $triggerOnReload = false;

    public function jsExecute(): JsBlock
    {
        $this->assertIsInitialized();

        return new JsBlock([(new Jquery($this->getOwner() /* TODO element and loader element should be passed explicitly */))->atkAjaxec([
            'url' => $this->getJsUrl(),
            'urlOptions' => $this->args,
            'confirm' => $this->confirm,
            'apiConfig' => $this->apiConfig !== [] ? $this->apiConfig : null,
            'storeName' => $this->storeName,
        ])]);
    }

    /**
     * Set a confirmation to be displayed before actually sending a request.
     *
     * @param string $text
     */
    public function setConfirm($text = 'Are you sure?'): void
    {
        $this->confirm = $text;
    }

    /**
     * @param \Closure(Jquery, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed, mixed): (JsExpressionable|View|string|void) $fx
     * @param array<int|string, string|JsExpressionable>                                                                                  $args
     *
     * @return $this
     */
    #[\Override]
    public function set($fx = null, $args = null)
    {
        if (!$fx instanceof \Closure) {
            throw new \TypeError('$fx must be of type Closure');
        }

        $this->args = [];
        foreach ($args ?? [] as $key => $val) {
            if (is_int($key)) {
                $key = $this->name . '_c' . $key;
            }
            $this->args[$key] = $val;
        }

        parent::set(function () use ($fx) {
            $chain = new Jquery();

            $values = [];
            foreach (array_keys($this->args) as $key) {
                $values[] = $this->getApp()->getRequestPostParam($key);
            }

            $response = $fx($chain, ...$values);

            // TODO should we create/pass $chain to $fx at all?
            if (count($chain->_chain) !== 0 && !$response instanceof JsExpressionable) {
                throw new Exception('Jquery JsCallback chain was mutated but not returned');
            }

            $ajaxec = $this->getAjaxec($response);

            $this->terminateAjaxIfCanTerminate($ajaxec);
        });

        return $this;
    }

    /**
     * A proper way to finish execution of AJAX response. Generates JSON
     * which is returned to frontend.
     */
    protected function terminateAjaxIfCanTerminate(JsBlock $ajaxec): void
    {
        $data = [
            'success' => true,
            'atkjs' => $ajaxec->jsRender(),
        ];

        if ($this->canTerminate()) {
            $this->getApp()->terminateJson($data);
        }
    }

    /**
     * Provided with a $response from callbacks convert it into a JavaScript code.
     *
     * @param JsExpressionable|View|string|null $response
     */
    public function getAjaxec($response): JsBlock
    {
        $jsBlock = new JsBlock();
        if ($response) {
            $jsBlock->addStatement($this->_getProperAction($response));
        }

        return $jsBlock;
    }

    #[\Override]
    public function getUrl(string $mode = 'callback'): string
    {
        throw new Exception('Do not use getUrl on JsCallback, use getJsUrl()');
    }

    /**
     * Transform response into proper JS Action and return it.
     *
     * @param View|string|JsExpressionable $response
     */
    private function _getProperAction($response): JsExpressionable
    {
        if ($response instanceof View) {
            $response = $this->_jsRenderIntoModal($response);
        } elseif (is_string($response)) { // TODO alert() should be removed
            $response = new JsExpression('alert([])', [$response]);
        }

        return $response;
    }

    private function _jsRenderIntoModal(View $response): JsExpressionable
    {
        if ($response instanceof Modal) {
            $html = $response->getHtml();
        } else {
            $modal = new Modal(['name' => 'js_callback_' . md5(random_bytes(16))]);
            $modal->setApp($this->getApp());
            $modal->add($response);
            $html = $modal->getHtml();
        }

        return new JsExpression('$([html]).modal(\'show\').data(\'needRemove\', true).addClass(\'atk-callback-response\')', ['html' => $html]);
    }
}