'How do I invoke inquirer.js menu in a loop using Promises?
I wrote a simple Node.js program with a nice menu system facilitated by inquirer.js. However, after selecting an option in the menu and completing some action, the program exits. I need the menu to show again, until I select the Exit [last] option in the menu. I would like to do this using Promise, instead of async/await.
I tried using a function to show the menu and called that function within a forever loop (E.g. while (true) { ... }
), but that made the program unusable. I changed that to a for-loop just to observe the problem. Below is the simple program and the resulting output.
PROGRAM
"use strict";
const inquirer = require('inquirer');
const util = require('util')
// Clear the screen
process.stdout.write("\u001b[2J\u001b[0;0H");
const showMenu = () => {
const questions = [
{
type: "list",
name: "action",
message: "What do you want to do?",
choices: [
{ name: "action 1", value: "Action1" },
{ name: "action 2", value: "Action2" },
{ name: "Exit program", value: "quit"}
]
}
];
return inquirer.prompt(questions);
};
const main = () => {
for (let count = 0; count < 3; count++) {
showMenu()
.then(answers => {
if (answers.action === 'Action1') {
return Promise.resolve('hello world');
}
else if (answers.action === 'Action2') {
return new Promise((resolve, reject) => {
inquirer
.prompt([
{
type: 'input',
name: 'secretCode',
message: "Enter a secret code:"
}
])
.then(answers => {
resolve(answers);
})
});
}
else {
console.log('Exiting program.')
process.exit(0);
}
})
.then((data) => { console.log(util.inspect(data, { showHidden: false, depth: null })); })
.catch((error, response) => {
console.error('Error:', error);
});
}
}
main()
OUTPUT
? What do you want to do? (Use arrow keys)
❯ action 1
action 2
Exit program ? What do you want to do? (Use arrow keys)
❯ action 1
action 2
Exit program ? What do you want to do? (Use arrow keys)
❯ action 1
action 2
Exit program (node:983) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 keypress listeners added to [ReadStream]. Use emitter.setMaxListeners() to increase limit
How can I block after the first call to generate the menu, wait for an option to be selected and the corresponding action to complete, and then cycle back to the next iteration of showing the menu?
Solution 1:[1]
You can use async/await
syntax:
Declare your main
function async
, and await
the returned Promise from inquirer:
const main = async () => {
for (let count = 0; count < 3; count++) {
await showMenu()
.then(answers => {
[...]
}
};
Your code doesn't work as you expect because, in short, the interpreter executes synchronous code before running any callbacks (from promises). As a consequence your synchronous for
loop executes before any I/O callbacks are resolved. All calls to showMenu()
returns promises which are resolved asynchronously, meaning nothing will be printed, and no inputs will be interpreted until after looping.
Writing await
blocks succeeding synchronous code inside an async
function, which is what it seems you're trying to do.
Solution 2:[2]
Using your code as a starting point, I hacked together my own library for displaying cli menus. It strips away a lot of Inquirer's boilerplate, letting you declare a menu graph/tree concisely.
The main.ts file shows how you use it. You declare a dictionary of MenuPrompts, which you add Menus, Actions and LoopActions to. Each prompt has a key, which other prompts can route to.
// main.ts
import { Menu, Action, MenuPrompt, openMenuPrompt, LoopAction } from "./menus";
// Set of prompts
let prompts = {
menu_1: new MenuPrompt("Menu 1 - This list is ordinal - What would like to do?", 20, true, [
new Menu("Menu 2", "menu_2"),
new LoopAction("Action", () => console.log("Menu 1 action executed")),
new Action("Back", context => context.last),
new Action("Exit", () => process.exit(0)),
]),
menu_2: new MenuPrompt("Menu 2 - This list is NOT ordinal - What would like to do?", 20, false, [
new Menu("Menu 1", "menu_1"),
new LoopAction("Action", () => console.log("Menu 2 action executed")),
new Action("Back", context => context.last),
new Action("Exit", () => process.exit(0)),
]),
};
// Open the "menu_1" prompt
openMenuPrompt("menu_1", prompts);
This is the lib file, which contains types & the function for opening the initial prompt.
// menus.ts
import * as inquirer from "inquirer";
// MAIN FUNCTION
export let openMenuPrompt = async (current: string, prompts: Dict<MenuPrompt>, last?: string): Promise<any> => {
let answer: Answer = (await inquirer.prompt([prompts[current]])).value;
let next = answer.execute({current, last});
if (!next) return;
return await openMenuPrompt(next, prompts, current == next? last : current );
};
// PUBLIC TYPES
export class MenuPrompt {
type = "list";
name = "value";
message: string;
pageSize: number;
choices: Choice[];
constructor(message: string, pageSize: number, isOrdinalList: boolean, choices: Choice[]) {
this.message = message;
this.pageSize = pageSize;
this.choices = choices;
if (isOrdinalList) {
this.choices.forEach((choice, i) => choice.name = `${i + 1}: ${choice.name}`)
}
}
}
export interface Choice {
name: string;
value: Answer;
}
export class Action implements Choice {
name: string;
value: Answer;
constructor(name: string, execute: (context?: MenuContext) => any) {
this.name = name;
this.value = {execute};
}
}
export class LoopAction implements Choice {
name: string;
value: Answer;
constructor(name: string, execute: (context?: MenuContext) => any) {
this.name = name;
this.value = {execute: context => execute(context) ?? context.current};
}
}
export class Menu implements Choice {
name: string;
value: Answer;
constructor(name: string, menuKey: string) {
this.name = name;
this.value = {execute: () => menuKey};
}
}
// INTERNAL TYPES
type Dict<T = any> = {[key: string]: T};
interface Answer {
execute: (context: MenuContext) => any;
}
interface MenuContext {
current: string;
last: string;
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | aksomat |
Solution 2 | Arik |