HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Flattening multiple nested node readline questions

Submitted by: @import:stackexchange-codereview··
0
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:

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.

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!

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.