caubry
caubry

Reputation: 260

Multiple user inputs using Nodejs

I'm trying to write a script that asks three questions in a row, while waiting for user input in between each question.
It seems that I have difficulty understanding how to do this with the non-blocking nature of node.
Here's the code I'm running:

var shell = require('shelljs/global'),
    utils     = require('./utils'),
    readline  = require('readline'),
    fs        = require('fs'),
    path      = require('path');

var rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});    

setUserDefaultSettings();

function setUserDefaultSettings() {
    var defaultSettingsFile = find('DefaultSettings.json');
    var defaultSettingsJSON = [
        {
            macro: '__new_project_path__',
            question: 'Please enter the destination path of your new project: '
        },
        {
            macro: '__as_classes_path__',
            question: 'Please enter the path to your ActionScript Classes: ',
        },
        {
            macro: '__default_browser_path__',
            question: 'Please enter the path to your default browser: '

        }
    ];
    var settingsKeys = [];
    var index        = 0;

    if (!test('-f', 'UserSettings.json')) {
        cp(defaultSettingsFile, 'UserSettings.json');
    }

    var userSettingsFile = pwd() + path.sep + find('UserSettings.json');

    fs.readFile(userSettingsFile, 'utf8', function (err, data) {
        if (err) {
            echo('Error: ' + err);
            return;
        }
        data = JSON.parse(data);

        for(var attributename in data) {
            settingsKeys.push(attributename);
        }

        defaultSettingsJSON.forEach(function(key) {
            index++;
            // Check if macros have been replaced
            if (data[settingsKeys[index - 1]] === key.macro) {
                // Replace macros with user input.
                replaceSettingMacro(userSettingsFile, key.macro, key.question);
            }
        });
    });
}

function replaceSettingMacro(jsonFile, strFind, question) {
    askUserInput(question, function(strReplace) {
        sed('-i', strFind, strReplace, jsonFile);
    });
}

function askUserInput(string, callback) {
    rl.question(string, function(answer) {
        fs.exists(answer, function(exists) {
            if (exists === false) {
                echo('File ' + answer + ' not found!');
                askUserInput(string, callback);
            } else {
                callback(answer);
            }
        });
    });
}

Only the first question is asked, as the script continues execution while the user is inputting their answer. I understand why this is the case, but don't know how I can work around this.

Upvotes: 3

Views: 3943

Answers (3)

caubry
caubry

Reputation: 260

Below is the correct solution to my question, following @Preston-S advice:

var questionList = [];
var macroList    = [];

setUserDefaultSettings();

function setUserDefaultSettings() {
    var defaultSettingsFile = find('DefaultSettings.json');
    var defaultSettingsJSON = [
        {
            macro: '__new_project_path__',
            question: 'Please enter the destination path of your new project: '
        },
        {
            macro: '__as_classes_path__',
            question: 'Please enter the path to your ActionScript Classes: ',
        },
        {
            macro: '__default_browser_path__',
            question: 'Please enter the path to your default browser: '

        }
    ];

    if (!test('-f', 'UserSettings.json')) {
        cp(defaultSettingsFile, 'UserSettings.json');
    }

    userSettingsFile = pwd() + path.sep + find('UserSettings.json');

    var settingsKeys     = [];
    var index            = 0;
    var canAskQuestion   = false;

    fs.readFile(userSettingsFile, 'utf8', function (err, data) {
        if (err) {
            echo('Error: ' + err);
            return;
        }
        data = JSON.parse(data);

        for(var attributename in data) {
            settingsKeys.push(attributename);
        }

        defaultSettingsJSON.forEach(function(key) {
            index++;
            // Check if macros have been replaced
            if (data[settingsKeys[index - 1]] === key.macro) {
                // Replace macros with user input.
                questionList.push(key.question);
                macroList.push(key.macro);

                if (!canAskQuestion) {
                    askQuestion(function() {
                        copyTemplate();
                    });
                    canAskQuestion = true;
                }
            } else {
                copyTemplate();
            }
        });
    });
}

function askQuestion(callback) {
    replaceSettingMacro(userSettingsFile, macroList.shift(), questionList.shift(), function() {
        if (macroList.length < 1 || questionList.length < 1) {
            callback();
        } else {
            askQuestion(callback);
        }
    });
}

function replaceSettingMacro(jsonFile, strFind, question, callback) {
    askUserInput(question, function(strReplace) {
        sed('-i', strFind, strReplace, jsonFile);
        callback();
    });
}

function askUserInput(string, callback) {
    rl.question(string, function(answer) {
        fs.exists(answer, function(exists) {
            if (exists === false) {
                echo('File ' + answer + ' not found!');
                askUserInput(string, callback);
            } else {
                callback(answer);
            }
        });
    });
}

function copyTemplate() {
    rl.close();
}

Upvotes: 0

Will Stern
Will Stern

Reputation: 17579

The 2 ways I'd handle this:
a) use sync-prompt - would make it very easy

If you still wanted to handle it asynchronously,
b) I'd use a promise library such as When and do a When.all([array of promises]) before continuing.

Upvotes: 2

Preston S
Preston S

Reputation: 2771

This looks like an appropriate place to implement a queue. The queue will initiate tasks one at a time only starting the next after the previous has finished.

in your askUserInput method try something like:

var questionQueue = [];
function askUserInput(string, callback) {
    if(questionQueue.length == 0) {
        rl.question(string, function(answer) {
            fs.exists(answer, function(exists) {
                if (exists === false) {
                    echo('File ' + answer + ' not found!');
                    askUserInput(string, callback);
                } else {
                    callback(answer);

                    if(questionQueue.length > 0){
                        var question questionQueue.shift();
                        askUserInput(question.string, question.callback);
                    }
                }
            });
        });
    }
    else {
        questionQueue.push({string: string, callback: callback});
    }
}

Another option included extending your callback further up the call stack and invoke the next question when you receive the callback from the previous.

Upvotes: 2

Related Questions