Node.js movie app - Add Inquirer.js
Part 1: Movie app with Vorpal.js - Create the framework
In Part 1 we created the framework for our CLI application by installing figlet, Chalk and Vorpal. But our app still contains dummy data, so it’s time to make it interactive.
Inquirer.js
Inquirer.js is a command line interface, which makes the application interactive by advanced error handling, validation and input parsing.
Creating the first prompt
Let’s go to the project folder again and install Inquirer.js by typing npm install --save inquirer
. Once it’s done, we need to require
it at the top of index.js
:
const inquirer = require('inquirer');
We are now ready to write the first prompt.
Prompts. We need to start somewhere and if the user wants to search for a movie, the title of the movie (or some part of it when they are not sure about it) will help to get detailed information about it.
But what is a prompt?
Inquirer.js comes with a prompt
method, where we can define our message (or question) to the user, the input type and we can also provide some validation to make sure the user doesn’t do anything funky.
These are all parts of a question object which is placed inside an array, and that array will be the argument of prompt
.
The prompt will return a Promise
.
Promises can easily be handled with the async/await syntax. The function which will contain the prompt (inside vorpal.action
) is already marked with the async
keyword, if not, we can do it now.
Writing the prompt. We can now write the first prompt inside vorpal.action
. Delete the line that logs the “Coming soon!” message and paste the following code instead:
try {
const movieTitleByUser = await inquirer.prompt([
{
type: 'input',
name: 'movieTitle',
message: 'Enter the title or part of the title',
validate: (input) => {
if (input) return true;
return 'Please enter a title!';
}
}
]);
vorpal.log(chalk.green(JSON.stringify(movieTitleByUser, null, 2)));
} catch (error) {
vorpal.log(chalk.red(error));
}
A lot is going on here so let’s go step by step.
We await
the prompt
inside a try/catch
block. If an error occurs, the catch
block will catch and display it in red with the help of chalk
and vorpal.log
.
The try
part contains the more interesting part of the code.
Handling promises. Because inquirer.prompt
returns a Promise
, we need to handle it by unwrapping the promise and getting the value which the promise has been resolved with.
Before async/await
this could be done using then
but now it’s easier to use this syntax. await
will make the value inside the promise available and we can save it into the movieTitleByUser
variable.
The prompt. inquirer.prompt
accepts the question object(s) wrapped in an array. Depending on the type
of the input, the object can have different properties.
In our case the type
of the prompt is input
because we want the user to enter some characters of title of the movie they want more information about.
To help the user with the action we want them to take, we display a message
. This message will be shown in the terminal when the search
command is issued. In this case we ask the user to provide the title of the movie, or if they don’t know the exact title, just type some part of it.
Inquirer.js saves the user’s answer into an object and the name
property of the question object determines which key the saved response should be assigned to inside the response object. In our case it should be movieTitle
because the user response is just about title of the movie.
Finally, we can make some validation. The validate
property is a function whose argument is the user input (input
). If everything goes well and the user entered a valid title, the function returns true
and prompt
will save the user’s response.
If we don’t like what the user entered, we can return
a message, so that the user can think their answer again.
We won’t go crazy here with the validation, so the only thing we’ll validate is that the user should enter something, i.e. an empty string is not acceptable (if (input) return true
). In case of any other input we will save the answer.
Logging the answer. As mentioned above the user input is saved to an object as the value of the key given in the name
property. We can log this object to the console in green
but first it’s a good idea to JSON.stringify
it to make more readable.
Let’s save our work and run npm start
, then type search
. First we should see the question with the blinking cursor at the end of the line:
? Enter the title or part of the title
Inquirer.js is prompting us to provide a title. First, press “Enter” and the error message we put inside the validate
function will be seen:
>> Please enter a title!
The cursor stays next to the prompt, so we can now enter a title. I enter “Jason Bourne” but feel free to type something else. Your response will be logged to the console in green:
{
"movieTitle": "Jason Bourne"
}
As we expected it’ll be an object, where the key (movieTitle
) is what we provided for the name
property and the value is our answer.
Add more questions
The API we’ll receive the data from (coming in the next post) has an option for the type of the movie as well. We can specify whether we want to look for a Movie
, a particular Episode
, a Series
or it doesn’t matter (All
).
Extending the prompt. We can add a second object to the prompt array:
{
type: 'list',
name: 'movieType',
message: 'Select the type of the movie',
choices: ['Movie', 'Series', 'Episode', 'All'],
filter: (type) => type.toLowerCase()
}
This time we want the user to select from the options mentioned above. A suitable type
would be list
, where the user will see the options we present in the choices
array as a list. We can select a suitable message
for this question and will save the user response as the value of the movieType
property.
Filter. Another property of the question object is filter
, which (as its name suggests) filters the user’s choice and forwards the filtered result to the response object.
In this case, all we do is to convert the choice to lower case.
Now the object returned from prompt
will contain two properties. Let’s see this in action, save the file, type exit
in the terminal and start the script again with npm start
and search
.
The result. After you have entered the title of the movie, you should see something like this:
Movies$ search
? Enter the title or part of the title Jason Bourne
? Select the type of the movie (Use arrow keys)
> Movie
Series
Episode
All
The first element of the choices
array is listed first and the cursor is next to it. You can select the desired option with the arrow keys.
Let’s select All
for now and we will get the object with both the title and the type selected:
{
"movieTitle": "Jason Bourne",
"movieType": "all"
}
As we can see the value of movieType
has been converted to lower case.
Refactoring code
index.js
is getting large as we add more code to it and even more will follow, so this is a good time to refactor the code and extract the prompt in its own module.
Create the module. Let’s create a new folder called modules
in the project root and a file called user-inputs.js
inside modules
:
mkdir modules
cd modules
touch user-inputs.js
We will store all prompt-related functions in user-inputs.js
.
Let’s require
Inquirer.js at the top of the new file and at the same time, delete it from index.js
Create a new function called getMovieTitleFromUser
in the new user-inputs.js
file and copy the prompt:
const inquirer = require('inquirer');
const getMovieTitleFromUser = () => {
return inquirer.prompt([
{
type: 'input',
name: 'movieTitle',
message: 'Enter the title or part of the title',
filter: (title) => title.toLowerCase(),
validate: (input) => {
if (input) return true;
return 'Please enter a title!';
}
},
{
type: 'list',
name: 'movieType',
message: 'Select the type of the movie',
choices: ['Movie', 'Series', 'Episode', 'All'],
filter: (type) => type.toLowerCase()
}
]);
};
The function will return
the prompt.
We also need to make this function available for index.js
, so we need to export it:
module.exports = { getMovieTitleFromUser };
Getting the module. Now we need to access the prompt in index.js
, so let’s require
it at the top and await
it at the right place:
// ..
const { getMovieTitleFromUser } = require('./modules/user-inputs');
// ..
try {
const movieData = await getMovieTitleFromUser();
vorpal.log(chalk.green(JSON.stringify(movieData, null, 2)));
} catch (error) {
vorpal.log(chalk.red(error));
}
// ..
We can use object destructuring because it’s a really cool thing.
We await
getMovieTitleFromUser
inside the try
block. With the await
keyword we’ll receive the value which the prompt promise has been resolved with. In our case this will be the object with the user inputs (movieTitle
and movieType
), which we save into the variable called movieData
for the time being.
If anything goes wrong, we’ll be directed to the catch
block, where the error is logged to the console.
Title to lower case. To make life easier and more consistent, we can also convert the title of the movie to lower case.
The user can type the title in title case or lower case (or upper case, quite a few options are available); we don’t know for sure. We can though transfer the text the user entered; therefore our request to the API will be in lower case.
Let’s add a one-liner filter
to the first object inside the prompt:
filter: (title) => title.toLowerCase(),
Run again. Let’s run the script again to make sure it works after refactoring the code. It should.
We’ll get an object displayed in the console where both the title and the type are lower cased.
The full code looks like this:
// index.js
const chalk = require('chalk');
const figlet = require('figlet');
const vorpal = require('vorpal')();
const { getMovieTitleFromUser } = require('./modules/user-inputs')
figlet('Movies', (_, data) => {
console.log(chalk.cyan(data));
vorpal
.command('search', 'Start searching movies')
.action(async () => {
try {
const movieData = await getMovieTitleFromUser();
vorpal.log(chalk.green(JSON.stringify(movieData, null, 2)));
} catch (error) {
vorpal.log(chalk.red(error));
}
});
vorpal
.delimiter('Movies$')
.show();
});
and
// modules/user-inputs.js
const inquirer = require('inquirer');
const getMovieTitleFromUser = () => {
return inquirer.prompt([
{
type: 'input',
name: 'movieTitle',
message: 'Enter the title or part of the title',
filter: (title) => title.toLowerCase(),
validate: (input) => {
if (input) return true;
return 'Please enter a title!';
}
},
{
type: 'list',
name: 'movieType',
message: 'Select the type of the movie',
choices: ['Movie', 'Series', 'Episode', 'All'],
filter: (type) => type.toLowerCase()
}
])
};
module.exports = { getMovieTitleFromUser };
Conclusion
Great! We have successfully integrated Inquirer.js into the CLI app, which is slowly getting its shape.
In the next post we’ll use the user input to fetch movie data from the API and will display real movie data in the terminal.
Thanks for reading and see you next time.