Welcome back! In our previous lessons, we laid the groundwork for our travel planner application by creating a RESTful API with Flask and rendering a basic HTML page. Now, we are ready to enhance the user experience by adding client-side JavaScript. This lesson will focus on implementing JavaScript to handle form submissions, simulate API calls, and dynamically render content. By the end of this lesson, you will be able to create a more interactive and engaging application that responds to user input in real-time.
Reviewing the Project Structure
Before we dive into linking JavaScript to our HTML, let's focus on the specific file we are creating and its location within the project structure.
We are creating the travel-planner.js file inside the static/js/ directory. This file will contain the JavaScript code responsible for enhancing the interactivity of our travel planner application. By placing it in the static/js/ directory, we ensure that it is served correctly by Flask as a static asset, allowing us to link it to our index.html file for client-side functionality.
Linking JavaScript to HTML
Now that we have created the travel-planner.js file in the static/js/ directory, it's time to link this JavaScript file to our HTML page. This is done by including a <script> tag in the index.html file. In Flask, we use the url_for function to correctly reference the static JavaScript file. This ensures that our JavaScript code is loaded and ready to enhance the page's functionality.
Here's how your index.html file should look, with comments indicating where the rest of the code should be:
HTML
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Travel Planner</title></head><body> <!-- Form for trip planning... --> <!-- Results Section... --> <!-- Link to the JavaScript file for handling form submission and rendering results --> <script src="{{ url_for('static', filename='js/travel-planner.js') }}"></script></body></html>
This line of code is placed just before the closing </body> tag in your HTML file, ensuring that the JavaScript is loaded after the HTML content. By linking the JavaScript file, we enable the dynamic features and interactivity that will enhance the user experience of our travel planner application.
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal
Preventing Default Form Submission and Initializing UI Elements
With our JavaScript file linked to the HTML, we are ready to start writing the JavaScript code in the travel-planner.js file. Our first task is to set up the form to handle submissions in a way that allows us to process the data and update the UI dynamically without refreshing the page.
Step-by-Step Guide:
Add an Event Listener to the Form:
We begin by adding an event listener to the form. This listener will trigger an async function whenever the form is submitted. Using async allows us to use the await keyword inside the function, which is useful for handling asynchronous operations like simulating API calls.
Prevent Default Form Submission:
By default, submitting a form causes the page to reload. We want to prevent this behavior so that we can handle the form data with JavaScript. We do this by calling e.preventDefault().
Show Loading Spinner and Results Card:
To provide feedback to the user that their submission is being processed, we display a loading spinner. We also prepare the results card where the itinerary will be displayed by clearing any previous content.
Here's how you can implement these steps in your travel-planner.js file:
// Add an event listener to the form to handle submissionsdocument.getElementById('tripForm').addEventListener('submit', async function(e) { // Prevent the default form submission behavior e.preventDefault(); // Show loading spinner and results card document.getElementById('loadingSpinner').style.display = 'block'; document.getElementById('resultsCard').style.display = 'block'; document.getElementById('resultsContent').innerHTML = ''; // Further processing of the form data will be done here...});
In this code, we set up an event listener on the form with the ID tripForm to handle submissions. By calling e.preventDefault(), we stop the default form submission, which would normally reload the page. Instead, we display a loading spinner and prepare the results card by clearing any previous content. This approach allows us to process the form data and update the user interface dynamically, providing a seamless and interactive experience for the user.
Handling Form Data with FormData Object
After intercepting the form submission, we can access the form data using the FormData object. This object provides a convenient way to extract user input values from the form fields. We can then process these values, such as converting them to the appropriate data types for further use in our application logic.
document.getElementById('tripForm').addEventListener('submit', async function(e) { // Code to prevent default and initialize UI... // Create a FormData object to easily access form input values const formData = new FormData(this); // Extract the city name from the form data const city = formData.get('city'); // Extract and convert the number of days to an integer const days = parseInt(formData.get('days')); // Extract and convert the number of attractions per day to an integer const attractionsPerDay = parseInt(formData.get('attractions_per_day')); // Further processing of the form data will be done here...});
Using the FormData object simplifies the process of gathering and manipulating form data, allowing us to focus on the core functionality of our application.
Simulating API Calls with Error Handling and UI Feedback
In this context, simulating API calls allows us to develop and test the client-side logic and user interface before a real backend or external service is available. This approach helps ensure that our application's interactive features work as intended, even while the backend is still under development.
To create a realistic user experience, we simulate an API call using a try-catch-finally block. The try block contains a simulated delay using setTimeout, which mimics the time taken for an API response. We use the await keyword to pause the execution of the function until the simulated delay is complete, just as we would when waiting for a real API call to return data. If an error occurs during this process, the catch block displays an error message to the user. The finally block ensures that the loading spinner is hidden, providing a seamless user experience regardless of the outcome.
document.getElementById('tripForm').addEventListener('submit', async function(e) { // Code to prevent default, initialize UI, and handle form data... try { // Simulate a delay of 2 seconds to mimic an API call await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate and process API response data... } catch (error) { document.getElementById('resultsContent').innerHTML = ` <div> An error occurred while planning your trip. Please try again. </div> `; } finally { // Hide loading spinner document.getElementById('loadingSpinner').style.display = 'none'; }});
By implementing this structure, we provide users with feedback during the processing phase and handle any potential errors gracefully, ensuring a robust and user-friendly application.
Simulating Response Data
Setting Up the Itinerary Display
With the simulated response data in hand, the next step is to dynamically render the itinerary on the page. We start by initializing an HTML string that will hold the itinerary content and checking if the daily plans data exists and is in the expected format.
document.getElementById('tripForm').addEventListener('submit', async function(e) { // Code to prevent default, initialize UI, handle form data, simulate delay... try { // Simulate a delay and generate response data... // Initialize the HTML string for the itinerary container let html = '<div class="itinerary">'; // Check if daily plans exist and are in an array format if (data.daily_plans && Array.isArray(data.daily_plans)) { // Further processing of daily plans will be done here... } else { // Display a message if no itinerary data is available html += '<div>No itinerary data available</div>'; } // Code to inject the HTML content into the results section... } catch (error) { // Error handling... } finally { // Hide loading spinner... }});
In this code, we begin by creating a variable html that will store the HTML content for our itinerary. We initialize it with a div element that has a class of "itinerary" to serve as a container. We then check if the data.daily_plans property exists and is an array. This validation ensures that we have valid data to work with before attempting to render the itinerary. If the data is not available or not in the expected format, we display a message to the user indicating that no itinerary data is available.
Iterating Through Each Day's Plan
Once we've confirmed that the daily plans data exists, we need to iterate through each day's plan to construct the HTML content. This involves creating a structure that will display the day number, attractions, and meal suggestions for each day.
if (data.daily_plans && Array.isArray(data.daily_plans)) { // Iterate over each day's plan to construct the itinerary details data.daily_plans.forEach((day) => { // Code to construct HTML for each day... }); // Code to add overall tips...} else { html += '<div>No itinerary data available</div>';}
Here, we set up the structure for iterating over each day's plan in the data.daily_plans array. We use the forEach method to loop through each day, which allows us to access the details for that specific day. Inside this loop, we will construct the HTML content for each day's itinerary, including attractions and meal suggestions. After processing all days, we'll add overall tips to provide general advice for the trip. If the daily plans data is not available, we display a message to inform the user.
Building Detailed Daily Activities
Adding Travel Tips and Finalizing the Display
After displaying the daily plans, we add a section for overall travel tips if they are available. These tips provide general advice for the trip. Finally, we inject the constructed HTML content into the results section of the page, making the itinerary visible to the user.
if (data.daily_plans && Array.isArray(data.daily_plans)) { // Code to iterate over each day's plan... // Add overall tips if available if (data.overall_tips) { html += ` <div> <h6>Overall Tips</h6> <div> ${data.overall_tips} </div> </div> `; }} else { html += '<div>No itinerary data available</div>';}// Inject the HTML content into the results sectiondocument.getElementById('resultsContent').innerHTML = html;
In this final section, we check if overall tips are available by verifying that the data.overall_tips property exists. If it does, we append a new div to our HTML string that contains an h6 heading labeled "Overall Tips" and a div that displays the tips content. This provides users with general advice for their trip, such as what to pack or local customs to be aware of.
After constructing all the HTML content for the itinerary, we use the innerHTML property to inject this content into the element with the ID "resultsContent". This replaces any previous content in that element with our newly generated itinerary. By doing this, we dynamically update the page to display the travel plan without requiring a page refresh, providing a seamless and interactive user experience.
Recap and Summary
In this lesson, we explored how to enhance our travel planner application with client-side JavaScript. We linked a JavaScript file to our HTML, handled form submissions, simulated API calls, and dynamically rendered content. We also implemented error handling and UI feedback to improve the user experience. As you move on to the hands-on practice exercises, remember to apply these concepts and experiment with the JavaScript code. These steps are essential as we continue to build a full-featured application. Keep up the great work, and I look forward to seeing your progress!
// Add an event listener to the form to handle submissionsdocument.getElementById('tripForm').addEventListener('submit', async function(e) { // Prevent the default form submission behavior e.preventDefault(); // Show loading spinner and results card document.getElementById('loadingSpinner').style.display = 'block'; document.getElementById('resultsCard').style.display = 'block'; document.getElementById('resultsContent').innerHTML = ''; // Further processing of the form data will be done here...});
JavaScript
document.getElementById('tripForm').addEventListener('submit', async function(e) { // Code to prevent default and initialize UI... // Create a FormData object to easily access form input values const formData = new FormData(this); // Extract the city name from the form data const city = formData.get('city'); // Extract and convert the number of days to an integer const days = parseInt(formData.get('days')); // Extract and convert the number of attractions per day to an integer const attractionsPerDay = parseInt(formData.get('attractions_per_day')); // Further processing of the form data will be done here...});
JavaScript
document.getElementById('tripForm').addEventListener('submit', async function(e) { // Code to prevent default, initialize UI, and handle form data... try { // Simulate a delay of 2 seconds to mimic an API call await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate and process API response data... } catch (error) { document.getElementById('resultsContent').innerHTML = ` <div> An error occurred while planning your trip. Please try again. </div> `; } finally { // Hide loading spinner document.getElementById('loadingSpinner').style.display = 'none'; }});
To effectively test our application's functionality, we simulate response data that mimics what a real API might return. This involves creating mock data for the travel itinerary based on user input, such as the city, number of days, and attractions per day. By generating this data, we can ensure that our application processes and displays information correctly without relying on a live API.
In the code provided, we simulate response data to test how our application processes and displays it. This approach allows us to ensure that the user interface behaves as expected without relying on a live API. Here's a detailed explanation of what we did in the code:
Create Simulated API Response Data:
We construct an object named data that mimics the structure of a real API response. This object includes details such as the city, number of days, and daily plans.
Generate Daily Plans:
We loop through the number of days specified by the user and create a simple itinerary for each day. For each day, we generate a list of attractions based on the number of attractions per day specified by the user. Each attraction includes a name, category, estimated duration, and description.
Add Overall Tips:
We include a section for overall travel tips, providing general advice for the trip.
By simulating this data, we can proceed to test how the application processes and displays it. This ensures that the user interface behaves as expected, allowing us to verify that the dynamic rendering of the itinerary works correctly.
JavaScript
document.getElementById('tripForm').addEventListener('submit', async function(e) { // Code to prevent default, initialize UI, handle form data, simulate delay... try { // Simulate a delay and generate response data... // Initialize the HTML string for the itinerary container let html = '<div class="itinerary">'; // Check if daily plans exist and are in an array format if (data.daily_plans && Array.isArray(data.daily_plans)) { // Further processing of daily plans will be done here... } else { // Display a message if no itinerary data is available html += '<div>No itinerary data available</div>'; } // Code to inject the HTML content into the results section... } catch (error) { // Error handling... } finally { // Hide loading spinner... }});
JavaScript
if (data.daily_plans && Array.isArray(data.daily_plans)) { // Iterate over each day's plan to construct the itinerary details data.daily_plans.forEach((day) => { // Code to construct HTML for each day... }); // Code to add overall tips...} else { html += '<div>No itinerary data available</div>';}
For each day in the itinerary, we construct detailed HTML content that includes the day number, a list of attractions with their details, and meal suggestions. This creates a comprehensive view of the daily activities for the user.
In this implementation, we use template literals to construct the HTML content for each day's plan. For each day, we create a div with a class of "day-plan" and include an h6 heading that displays the day number. We then create an unordered list to display the attractions for that day. Using the map method, we iterate over each attraction and create a list item that includes the attraction's name, category, estimated duration, and description. The join('') method combines all the list items into a single string.
We also check if meal suggestions are available for the day using a conditional (ternary) operator. If meal suggestions exist, we create another section that lists them. This approach ensures that we only display the meal suggestions section if there are actual suggestions to show. The resulting HTML provides a detailed and organized view of each day's activities.
JavaScript
if (data.daily_plans && Array.isArray(data.daily_plans)) { // Code to iterate over each day's plan... // Add overall tips if available if (data.overall_tips) { html += ` <div> <h6>Overall Tips</h6> <div> ${data.overall_tips} </div> </div> `; }} else { html += '<div>No itinerary data available</div>';}// Inject the HTML content into the results sectiondocument.getElementById('resultsContent').innerHTML = html;