Introduction and Lesson Overview

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.

app/
├── main.py
├── travel_planner/
├── static/
│   └── js/
│       └── travel-planner.js  # JavaScript file for enhancing interactivity
└── templates/
    └── index.html

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:

<!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.

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:

  1. 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.

  2. 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().

  3. 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 submissions
document.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 section
document.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!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal