patternjavascriptMinor
Flattening multiple nested node readline questions
Viewed 0 times
nodequestionsreadlinenestedmultipleflattening
Problem
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:
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.
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.
Solution
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.
Output:
If you want the action to be chosen by the user, add these functions:
And change main procedure to:
Now output shoud be like:
In case of error (e.g. enter non-existent action 'action3'):
It's very easy to apply this solution to your problem. Just define your questions as:
Your callback with answers:
And the procedure may be unchanged - when a user has the ability to select a set of questions (actions):
Output:
Or if you want to apply to concrete questions set just use:
Output:
I hope it will be useful :)
Enjoy!
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: action3It'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!
Code Snippets
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);A1: Question 1: a
A1: Question 2: b
A1: Question 3: c
OUTPUT:
[ 'a', 'b', 'c' ]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));
});
}showInterface()
.then(askQuestionForActionKey)
.then(askQuestions)
.then(doSomethingwithAnswers)
.catch(handleError);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' ]Context
StackExchange Code Review Q#134048, answer score: 6
Revisions (0)
No revisions yet.