4
\$\begingroup\$

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.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Use promises, maybe with async/await. \$\endgroup\$
    – gcampbell
    Commented Jul 6, 2016 at 13:35

2 Answers 2

4
\$\begingroup\$

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();
\$\endgroup\$
2
  • \$\begingroup\$ Doesn't work for me. Just runs over the whole thing to the closing lambda without any of the series executing. \$\endgroup\$ Commented 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
    Commented Mar 21, 2019 at 9:48
6
\$\begingroup\$

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!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.