Capturing user input in a Node.js application

In a previous article, we saw how Node.js could accept parameters sent via the CLI. In that article, we mentioned that Node.js is also capable of "waiting for user input". This article discusses how to create an application in Node.js that will await input from a user and act on it.

readline

Node.js comes with a built-in module called readline that provides developers with an interface to read data from a Readable stream - a Readable stream being process.stdin.

Here's a very simple example that listens to user input and returns information about the key that was pressed:

const readline = require('readline');

readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);

process.stdin.on('keypress', (key, data) => {
  if (data.ctrl && data.name === 't') {
    process.exit();
  } else {
    console.log('key', key);
    console.log('data', data);
  }
});
console.log('Press a key');

setRawMode tells the terminal to make the characters available one-by-one. This pretty much means that pressing a key allows us to capture it and act on it - execute an action or do something else.

In the example above we print the key pressed as well as some additional data, for example, was the 'ctrl' key pressed? Pressing these modifier keys return a boolean value. Please find the result of running the application below - pressing the key 'a' followed by 'ctrl-b':

key a
data { sequence: 'a',
  name: 'a',
  ctrl: false,
  meta: false,
  shift: false }
key
data { sequence: '\u0002',
  name: 'b',
  ctrl: true,
  meta: false,
  shift: false }

Since we are capturing all the keys that are pressed we need a condition to be able to escape this loop - we can do this by capturing a combination of 'ctrl-t' - this is visible in the code sample above as well.

A more sophisticated example

Let's create a slightly better example where we are going to have a function to retrieve weather data based on some keys pressed. We'll have a key mapping - a character to a city, and based on which key is pressed, lookup the weather information and return it.

Key mapping

To do a keymapping, we'll utilise the ES2015 Maps in JavaScript. Simply put, it's a key value structure with helper methods to get a value based on a key:

const map = new Map();
map.set('b', 'Budapest');
map.set('h', 'Helsinki');
map.set('l', 'London');
map.set('r', 'Rome');
map.set('s', 'Stockholm');

We can print out these values using a for...of loop:

console.log('Use the following key mappings or press ctrl-t to exit:')
for (const [key, value] of map.entries()) {  
  console.log(`${key} = ${value}`);
}

Let's also have a function in place to retrieve weather data:

function getWeatherData(city) {
  const url = `http://api.openweathermap.org/data/2.5/weather?units=metric&appid=YOUR_API_KEY&q=${city}`;
  return new Promise((resolve, reject) => {
    const lib = url.startsWith('https') ? require('https') : require('http');
    const request = lib.get(url, response => {
      if (response.statusCode < 200 || response.statusCode > 299) {
         return reject(new Error('Failed to load page, status code: ' + response.statusCode));
       }
      const body = [];
      response.on('data', chunk => body.push(chunk));
      response.on('end', () => {
        const data = body.join('');
        const parsed = JSON.parse(data);
        const final = `The weather in ${city} is ${parsed.main.temp }°C.`;
        return resolve(final);
      });
    });
    request.on('error', err => reject(err))
  });
}

In the example above we are using the Openweather API - please go ahead and register for a free API key to be able to run this sample.

Let's create a listener and attach that to the keypress event, just like how we have done it before, but this time the code is going to be slightly more complex since we'll need to collect the weather data:

async function listener(key, data) {
  if (data.ctrl && data.name === 't') {
    process.exit();
  } else {
    if (map.has(key)) {
      const city = map.get(key);
      console.log(await getWeatherData(city));
    } else {
      console.log(`"${key} is not defined as a key mapping. Please type in a city instead.`);
      removeAndExceptLine();
    }
  }
}

process.stdin.on('keypress', listener);

Last but not least, if someone presses a key, that's not in our list, we allow the user to input a city, remove the event listener for the keypress, and create a new TTY interface which will allow us to capture a line entry:

function removeAndExceptLine() {
  process.stdin.removeListener('keypress', listener);
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });
  rl.on('line', async input => {
    const city = input;
    console.log(await getWeatherData(city));
    rl.close();
  });
}

And that concludes the application, let's go ahead and test it out:

Use the following key mappings or press ctrl-t to exit:
b = Budapest
h = Helsinki
l = London
r = Rome
s = Stockholm
The weather in Helsinki is -3°C.
The weather in Rome is 14.1°C.
"x is not defined as a key mapping. Please type in a city instead.
Paris
The weather in Paris is 6.37°C.

Conclusion

In this article, we have reviewed how to work with the readline built-in module to capture multiple inputs from a user, and we have also created a simple application around it using these concepts.