Say I'm creating a simple CLI. I want to use native node readline module to take in some input from user at prompt. I thought of this:
var prompt = chalk.bold.magenta;
var info = {};
rl.question(prompt('Thing One : '), function(args) {
info.one = args;
rl.question(prompt('Thing Two : '), function(args) {
info.two = args;
rl.question(prompt('Thing Three : '), function(args) {
info.three = parseInt(args);
rl.close();
runSomeOtherModuleNow();
})
})
});
This does seem to work in a way I'd like, but this seems like a bad approach. I'd much prefer a flatter code than a pyramid like this.
-
1\$\begingroup\$ Use promises, maybe with async/await. \$\endgroup\$gcampbell– gcampbell2016年07月06日 13:35:11 +00:00Commented Jul 6, 2016 at 13:35
2 Answers 2
Flow Control libraries such as Async.js exist for exactly that. With async, your code can become:
var async = require('async');
var prompt = chalk.bold.magenta;
var info = {};
async.series([
(callback) => {
rl.question(prompt('Thing One : '), function(args) {
info.one = args;
callback();
}
},
(callback) => {
rl.question(prompt('Thing Two : '), function(args) {
info.two = args;
callback();
}
},
(callback) => {
rl.question(prompt('Thing Three : '), function(args) {
info.three = parseInt(args);
callback();
}
}
], () => {
rl.close();
runSomeOtherModuleNow();
});
EDIT: Nowaday, we have Promises and async/await, so the code could be shortened to somthing like:
const util = require('util'),
question = util.promisify(rl.question),
prompt = chalk.bold.magenta,
info = {};
info.one = await question(prompt('Thing One : '));
info.two = await question(prompt('Thing Two : '));
info.three = await question(prompt('Thing Three : '));
rl.close();
runSomeOtherModuleNow();
-
\$\begingroup\$ Doesn't work for me. Just runs over the whole thing to the closing lambda without any of the series executing. \$\endgroup\$Dmitri Nesteruk– Dmitri Nesteruk2019年03月20日 20:46:59 +00:00Commented Mar 20, 2019 at 20:46
-
\$\begingroup\$ @DmitriNesteruk: Maybe you want to open a new question here or on stack overflow with your code then, also, nowaday JS have Promises and async/await that make this kind of code even easier. \$\endgroup\$DrakaSAN– DrakaSAN2019年03月21日 09:48:41 +00:00Commented Mar 21, 2019 at 9:48
Also, I was looking for an answer how to flatten (and automate) calls to rl.question(). In my solution I used Promises - chained - to display questions sequentially.
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const QUESTIONS = {
action1: ['A1: Question 1', 'A1: Question 2', 'A1: Question 3'],
action2: ['A2: Question 1', 'A2: Question 2', 'A2: Question 3']
}
let askQuestions = (actionKey) => {
return new Promise( (res, rej) => {
let questions = QUESTIONS[actionKey];
if(typeof questions === 'undefined') rej(`Wrong action key: ${actionKey}`);
let chainQ = Promise.resolve([]); // resolve to active 'then' chaining (empty array for answers)
questions.forEach(question => {
chainQ = chainQ.then( answers => new Promise( (resQ, rejQ) => {
rl.question(`${question}: `, answer => { answers.push(answer); resQ(answers); });
})
);
});
chainQ.then((answers) => {
rl.close();
res(answers);
})
});
};
let handleError = (err) => {
console.log(`ERROR: ${err}`);
}
let doSomethingwithAnswers = (answers) => {
return new Promise( (res, rej) => {
console.log('OUTPUT:');
console.dir(answers);
});
}
askQuestions('action1')
.then(doSomethingwithAnswers)
.catch(handleError);
Output:
A1: Question 1: a
A1: Question 2: b
A1: Question 3: c
OUTPUT:
[ 'a', 'b', 'c' ]
If you want the action to be chosen by the user, add these functions:
let showInterface = () => {
return new Promise( (res, rej) => {
console.log('Select action (enter action name):')
console.log('-'.repeat(30));
Object.keys(QUESTIONS).forEach(actionKey => {
console.log(`${actionKey}`);
});
console.log('-'.repeat(30));
res();
});
};
let askQuestionForActionKey = () => {
return new Promise( (res, rej) => {
rl.question('Action key: ', actionKey => res(actionKey));
});
}
And change main procedure to:
showInterface()
.then(askQuestionForActionKey)
.then(askQuestions)
.then(doSomethingwithAnswers)
.catch(handleError);
Now output shoud be like:
Select action (enter action name):
------------------------------
action1
action2
------------------------------
Action key: action1
A1: Question 1: a
A1: Question 2: b
A1: Question 3: c
OUTPUT:
[ 'a', 'b', 'c' ]
In case of error (e.g. enter non-existent action 'action3'):
Select action (enter action name):
------------------------------
action1
action2
------------------------------
Action key: action3
ERROR: Wrong action key: action3
It's very easy to apply this solution to your problem. Just define your questions as:
const QUESTIONS = {
sequence: ['Thing One', 'Thing Two', 'Thing Three']
};
Your callback with answers:
let doSomethingwithAnswers = (answers) => {
return new Promise( (res, rej) => {
console.log('Make stuff with answers:');
console.dir(answers);
});
}
And the procedure may be unchanged - when a user has the ability to select a set of questions (actions):
showInterface()
.then(askQuestionForActionKey)
.then(askQuestions)
.then(doSomethingwithAnswers)
.catch(handleError);
Output:
Select action (enter action name):
------------------------------
sequence
------------------------------
Action key: sequence
Thing One: a
Thing Two: b
Thing Three: c
Make stuff with answers:
[ 'a', 'b', 'c' ]
Or if you want to apply to concrete questions set just use:
askQuestions('sequence')
.then(doSomethingwithAnswers)
.catch(handleError);
Output:
Thing One: a
Thing Two: b
Thing Three: c
Make stuff with answers:
[ 'a', 'b', 'c' ]
I hope it will be useful :) Enjoy!
Explore related questions
See similar questions with these tags.