Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request - ability to add sub-menu from addItem's callable #277

Open
temuri416 opened this issue Nov 4, 2023 · 9 comments
Open

Comments

@temuri416
Copy link

Something along these lines:

    $builder = (new CliMenuBuilder)
        ->addItem('Load Available Options', function (CliMenu $menuItem) {
            /**
             * Here I would load available options from DB and add them as $menu's subMenu
             */
            $loadedOptions = ['Opt1', 'Opt2', 'Opt3'];

            $builder = $menuItem->getBuilder();
            $subMenu = $builder->addSubMenu('Options');

            foreach ($loadedOptions as $opt) {
                $subMenu->addItem($opt);
            }

            $builder->build();
        })
        ->build();

    $builder->open();

In other words, a support for dynamically adding submenus when a menu item is selected would be an amazing feature.

Keep up the great work!

Thank you :)

@AydinHassan
Copy link
Member

I guess you can already achieve this, maybe not so simple, let me take a look

@AydinHassan
Copy link
Member

Is this what you mean?

<?php
declare(strict_types=1);

use PhpSchool\CliMenu\CliMenu;
use PhpSchool\CliMenu\Builder\CliMenuBuilder;
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;

require_once(__DIR__ . '/../vendor/autoload.php');

$itemCallable = function (CliMenu $menu) {
    echo $menu->getSelectedItem()->getText();
};

$menu = (new CliMenuBuilder)
    ->setTitle('CLI Menu')
    ->addSubMenu('Options', function (CliMenuBuilder $b) {
        $b->setTitle('CLI Menu > Options');
    })
    ->addItem('Load Available Options', function (CliMenu $menu) {
        $items = $menu->getItems();

        //find options menu
        /** @var MenuMenuItem $optionsMenu */
        $optionsMenu = current(array_values(array_filter($items, function (MenuItemInterface $item) {
            return $item instanceof MenuMenuItem && $item->getText() === 'Options';
        })));

        if ($optionsMenu === false) {
            //menu not found
        }
        $optionsMenu->getSubMenu()->setItems([
            new \PhpSchool\CliMenu\MenuItem\SelectableItem('Option 1', function (CliMenu $menu) {
                echo $menu->getSelectedItem()->getText();
            }),
            new \PhpSchool\CliMenu\MenuItem\SelectableItem('Option 2', function (CliMenu $menu) {
                echo $menu->getSelectedItem()->getText();
            })
        ]);

        echo "Loaded";
    })
    ->setBackgroundColour('yellow')
    ->build();

$menu->open();

@temuri416
Copy link
Author

First of all, thank you for your time and the example.

The solution above kind of works, but I wonder if it can be improved to the following flow.

Step 1.
Menu items are:

Cli Menu

● OPTIONS
● EXIT

Step 2.
User enters OPTIONS menu item by pressing ENTER. The $itemCallable registered with OPTIONS menu item then loads options from the database and programmatically builds submenu with its options, so that:

Step 3.
Menu items are:

Cli Menu > Options

● Option 1
● Option 2
● Option 3
● GO BACK

Selecting any of the options above would take user to the root, which would look as:

Step 4.
Upon return to the root menu the items are:

Cli Menu

● OPTIONS
● EXIT

That's it.

In other words, I'd like to generate sub-menu of a root menu item dynamically. I hope this explains it :-)

Cheers!

@AydinHassan
Copy link
Member

AydinHassan commented Nov 5, 2023

Ah yeah, I thought that's what you really meant, you can do that by extending the submenu item:

<?php
declare(strict_types=1);

use PhpSchool\CliMenu\CliMenu;
use PhpSchool\CliMenu\Builder\CliMenuBuilder;
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;

require_once(__DIR__ . '/../vendor/autoload.php');

$itemCallable = function (CliMenu $menu) {
    echo $menu->getSelectedItem()->getText();
};

class DynamicSubMenu extends MenuMenuItem
{
    public function __construct(string $text, string $title)
    {
        parent::__construct($text, new CliMenu($title, []), false);
    }

    public function getSelectAction() : ?callable
    {
        return function (CliMenu $menu) {

            $this->getSubMenu()->setItems([
                new \PhpSchool\CliMenu\MenuItem\SelectableItem('Option 1', function (CliMenu $menu) {
                    echo $menu->getSelectedItem()->getText();
                }),
                new \PhpSchool\CliMenu\MenuItem\SelectableItem('Option 2', function (CliMenu $menu) {
                    echo $menu->getSelectedItem()->getText();
                })
            ]);

            $this->showSubMenu($menu);
        };
    }
}

$menu = (new CliMenuBuilder)
    ->setTitle('CLI Menu')
    ->addMenuItem(new DynamicSubMenu('Options', 'CLI Menu > Options'))
    ->setBackgroundColour('yellow')
    ->build();

$menu->open();

It would be cool to support this natively though. I guess we could accept a callable parameter to MenuMenuItem constructor, and if you specify it, we call it with the existing select action as a parameter (which just calls showSubMenu). Then the consumer decides when to forward, instead of us deciding whether we open before or after. If you don't provide the callable arg then we just use our existing action. I would be happy to accept this as a PR if you wanna work on it :)

So MenuMenuItem's getSelectAction method would look something like:

public function getSelectAction() : ?callable
{
    $show = function (CliMenu $menu) {
        $this->showSubMenu($menu);
    };
    
    if ($this->customAction === null) {
        return $open;
    }

    ($this->customAction)($open);
}

@temuri416
Copy link
Author

Yes, that's what I had in mind. I'd have to dig deep into the source code to prepare a PR. I'll try to spend some time on it.

Cheers!

@temuri416
Copy link
Author

Got a question regarding your latest solution.

The "GoBack" action does not return from the DynamicSubMenu back to its parent:

        $this->getSubMenu()->setItems([
            new SelectableItem('Option 1', function (CliMenu $menu) {
                echo $menu->getSelectedItem()->getText();
            }),
            new SelectableItem('Option 2', function (CliMenu $menu) {
                echo $menu->getSelectedItem()->getText();
            }),
            new SelectableItem('Back', new GoBackAction)
        ]);

I suppose that's because there's no proper support for dynamic submenus?

@temuri416
Copy link
Author

Yeah, that seems to be the case:

public function __invoke(CliMenu $menu) : void
{
    if ($parent = $menu->getParent()) {
        $menu->closeThis();
        $parent->open();
    }
}

$parent is missing.

@AydinHassan
Copy link
Member

AydinHassan commented Nov 5, 2023

Yes probably a few things don't work properly, I'm sure you could hack them in, but would be nice to have baked in support, feel free to ping if you have any questions !

eg something hacky like $this->getSubMenu()->setParent($menu); in the select action of DynamicSubMenu

@temuri416
Copy link
Author

Maybe the key is to make the following work:

->addSubMenu(new DynamicSubMenu('Options', 'CLI Menu > Options'))

Started digging in the source now, hopefully will be able to figure out proper solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants