Properly using Modal->set() when in need of re-use?


#1

Hello, I’m having problems figuring out how to properly set up my code so that I can create a single modal and use it in PHP.

Currently I have something along the lines of

$this->buttonOpen->on('click, $this->somewhere->modal->show());
$this->somewhere->modal->set(function($m){
    $m->add(['Text', 'content' => 'AAA']);
});

But I’d like to re-use this component multiple times, say for example, a window for renaming something.
I’d need to keep some reference to the PHP thing it’s supposed to change and I’d like to load it all from PHP itself, how can I do it without creating multiple modals? Is it even doable?
I’ve thought about maybe modifying the set() function to allow it to take an unique identifier which will be included in the url, so it can see if the set() call is actually supposed to run right now, or later.

Is there a proper way to do it that I’ve been unable to find, or is this not really meant to be used this way?
I’m open to suggestions but I need it to run all on PHP, not jsModal and such.


#2

Well I’ve looked into how ATK UI generates Modals, thought about it and came up with a solution, it’s not the most graceful one, but it’s all based in PHP with not much code that’s actually in JS.

Basically all you need is a modified Modal class, which supports multiple “unique keys”, as I’ve called them.

Once you do that, all you need is to make a modal generate a function, that takes the key as a parameter, runs the $(modal).data() method, which updates the url for the modal (thank god this actually works or it’d be much more painful), and that’s about it

Edit: well the code kinda likes to explode still so I’ll fix it up and get a working version here when it’s fine.


#3

Instead of editing the other post 50 times I’ll just dump it here.

<?php
/**
 * Created by PhpStorm.
 * User: Pawel K. [pawel.k@icenter.pl]
 * Date: 19.03.2019
 * Time: 11:41
 */

namespace atk4\ui;


class UniqueModal extends Modal {
    public $cb_views = [];
    public $cbs = [];
    public $unique_fx = [];
    public $warning_text        =   'Invalid modal action specified!';

    public $hideCloseButton     =   [];

    const FUNCTION_NAME         =   'atk_unique_modal_update_show_';

    private $modalUniqueSetCounter = 0;

    /**
     * Set callback function for this modal.
     *
     * @param callable|array $fx
     * @param string|null $uniqueKey
     *
     * @throws Exception
     * @return string
     */
    public function set($fx = [], $uniqueKey = null)
    {
        if (!is_object($fx) && !($fx instanceof \Closure)) {
            throw new Exception('Error: Need to pass a function to UniqueModal::set()');
        }

        if ($uniqueKey === null)
            $uniqueKey .= $this->name.'_c_unique_'.$this->modalUniqueSetCounter++;

        if (array_key_exists($uniqueKey, $this->unique_fx))
            throw new Exception('Error: Unique key already exists in UniqueModal::set()');

        $this->unique_fx[$uniqueKey] = [$fx];
        $this->enableCallback($uniqueKey);

        return $uniqueKey;
    }

    /**
     * Add View to be loaded in this modal and
     * attach CallbackLater to it.
     * The cb_view only will be loaded dynamically within modal
     * div.atk-content.
     */
    public function enableCallback($uniqueKey = null)
    {
        $this->cb_views[$uniqueKey] = $this->add('View');
        $this->cb_views[$uniqueKey]->stickyGet('__atk_m', $this->name.'_'.$uniqueKey);
        $this->cbs[$uniqueKey] = $this->cb_views[$uniqueKey]->add('CallbackLater');

        $this->cbs[$uniqueKey]->set(function() use ($uniqueKey) {
             if ($this->cbs[$uniqueKey]->triggered() && isset($this->unique_fx[$uniqueKey]))
                 $this->unique_fx[$uniqueKey][0]($this->cb_views[$uniqueKey]);

             $modalName = $_GET['__atk_m'] ?? null;
             if ($modalName === $this->name.'_'.$uniqueKey)
                 $this->app->terminate($this->cb_views[$uniqueKey]->renderJSON());
        });
    }

    /**
     * Set modal to show on page.
     * Will trigger modal to be show on page.
     * ex: $button->on('click', $modal->show());.
     *
     * @param string|null $uniqueKey
     *
     * @return mixed
     */
    public function show($uniqueKey = null)
    {
        return new jsExpression(self::FUNCTION_NAME.$this->name.'([])', [$uniqueKey]);
    }

    /**
     * @param null|string $uniqueKey
     * @return $this
     */
    public function notClosable($uniqueKey = null) {
        if (!$uniqueKey)
            return $this;

        $this->hideCloseButton[] = $uniqueKey;
        parent::notClosable();
        return $this;
    }

    private function getModalData() {
        $data = [];
        $data['type'] = $this->type;
        $data['label'] = $this->loading_label;

        if (!empty($this->args)) {
            $data['args'] = $this->args;
        }

        return $data;
    }

    public function getExpression($data = null) {
        if ($data === null)
            $data = $this->getModalData();

        $template =
            'window.'.self::FUNCTION_NAME.$this->name.' = function(uniqueKey) {
                    var actions = [actions];
                    if (!(uniqueKey in actions)) {
                        console.warn([warning]);
                        return;
                    }
                    var nonCloseAble = [non_close];                   
                    $([modal_id]).data(actions[uniqueKey]);
                    $([modal_id]).modal("show");
                    
                    if (nonCloseAble.indexOf(uniqueKey) !== -1) {
                        $([modal_id]).find(".close").hide();
                    }
                    else {
                        $([modal_id]).find(".close").show();
                    }
                }';

        $actions = [];
        foreach ($this->cbs as $key => $callback)
            $actions[$key] = array_merge($data, ['uri' => $callback->getJSURL()]);

        return new jsExpression($template, ['actions' => $actions, 'warning' => $this->warning_text, 'modal_id' => '#'.$this->id, 'non_close' => $this->hideCloseButton]);
    }

    public function renderView()
    {
        if (!empty($this->title)) {
            $this->template->trySet('title', $this->title);
            $this->template->trySet('headerCss', $this->headerCss);
        }

        if (!$this->showActions) {
            $this->template->del('ActionContainer');
        }

        // call modal creation first
        if (isset($this->options['modal_option'])) {
            $this->js(true)->modal($this->options['modal_option']);
        } else {
            $this->js(true)->modal();
        }

        //add setting if available.
        if (isset($this->options['setting'])) {
            foreach ($this->options['setting'] as $key => $value) {
                $this->js(true)->modal('setting', $key, $value);
            }
        }

        $this->template->trySet('close', 'icon close');

        $data = $this->getModalData();

        if (count($this->unique_fx))
            $this->_js_actions[true][] = $this->getExpression($data);
        else
            $this->js(true)->data($data);

        View::renderView();
    }
} 

The code above can be used in any project with an author notice (code comment is fine) (no warranty/support and all that)

You use it like a normal modal, but now it’s re-usable.

$modal = $app->add(['UniqueModal']);

$uniqueKey = $modal->set(function($m) {
    // normal modal code here
});

$uniqueKey2 = $modal->set(function($m) {
    // normal code here again
});

$app->add(['Button', 'Press me!'])->on('click', $modal->show($uniqueKey));
$app->add(['Button', 'Press me two!'])->('click', $modal->show($uniqueKey2)); 

If you need to refresh the js code (which happens if you modify something related to ->set(), you can do that by returning $modal->getExpression() along with your other js actions (suggested use is at the end), and you need to remember that this could change in the set() function directly.
I highly suggest using your own unique keys (if you register a function under something like rename (second argument in ->set()) it’ll never change and thus be fine.

Let me know if you notice any weird behaviour.