Introduction to Client-Side Interactivity

Welcome to the third lesson in our course on building an image generation web application with Flask! In our previous lessons, we've built a solid foundation for our application by creating the HTML structure and styling it with CSS. We now have a visually appealing interface with tabs, form elements, and containers for displaying generated images.

However, our application is still static — clicking buttons doesn't do anything, and there's no way to actually generate images or switch between tabs. This is where JavaScript comes in. JavaScript is the programming language that brings web pages to life by adding interactivity and dynamic behavior.

In this lesson, we'll focus on implementing the client-side functionality that will allow users to interact with our application. We'll write JavaScript code to handle tab navigation, capture user input, send requests to our backend API, and display the generated images.

The JavaScript file we'll be creating will serve as the bridge between our user interface and the backend Flask application. It will handle all the client-side logic, from validating user input to making API calls and updating the DOM based on the responses.

Let's get started by implementing the tab navigation functionality, which will allow users to switch between the "Generate Image" and "View History" tabs.

Implementing Tab Navigation

Our application has two main tabs: "Generate Image" and "View History." We need to implement a function that will show the selected tab and hide the others when a user clicks on a tab button.

Let's create a file named script.js in the static folder of our Flask application. This is the same folder where we placed our style.css file in the previous lesson.

1app/ 2├── templates/ 3│ └── index.html 4└── static/ 5 ├── style.css 6 └── script.js

Now, let's implement the openTab() function that will handle the tab switching:

JavaScript
1function openTab(tabName) { 2 document.querySelectorAll('.tab-content').forEach(tab => { 3 tab.style.display = 'none'; 4 }); 5 document.getElementById(tabName).style.display = 'block'; 6}

This function does two main things:

  1. First, it selects all elements with the class tab-content using document.querySelectorAll() and hides them by setting their display style property to 'none'.
  2. Then, it selects the specific tab content element with the ID matching the tabName parameter using document.getElementById() and shows it by setting its display style property to 'block'.

When a user clicks on a tab button, this function will be called with the ID of the corresponding tab content as the argument. For example, when the user clicks on the "Generate Image" tab button, the function will be called as openTab('generate'), and when they click on the "View History" tab button, it will be called as openTab('history').

Remember that in our HTML, we've already set up the tab buttons to call this function with the appropriate arguments:

HTML, XML
1<button class="tab-button" onclick="openTab('generate')">Generate Image</button> 2<button class="tab-button" onclick="openTab('history')">View History</button>

With this function in place, users can now switch between tabs by clicking on the tab buttons. The selected tab's content will be displayed, while the other tab's content will be hidden.

Now that we have the tab navigation working, let's move on to implementing the image generation functionality.

Building the Image Generation Function

The core functionality of our application is generating images based on user input. We need to implement a function that will capture the user's input, validate it, and send it to our backend API.

Let's add the generateImage() function to our script.js file:

JavaScript
1function generateImage() { 2 const promptInput = document.getElementById('prompt').value; 3 const aspectRatio = document.getElementById('aspect-ratio').value; 4 const loadingMessage = document.getElementById('loading-message'); 5 const generateButton = document.getElementById('generate-button'); 6 const imageContainer = document.getElementById('image-container'); 7 8 if (!promptInput) { 9 alert('Please enter a prompt'); 10 return; 11 } 12 13 loadingMessage.style.display = 'block'; 14 generateButton.style.display = 'none'; 15 imageContainer.innerHTML = ''; 16 17 fetch('/api/generate_image', { 18 method: 'POST', 19 headers: { 'Content-Type': 'application/json' }, 20 body: JSON.stringify({ user_input: promptInput, aspect_ratio: aspectRatio }) 21 }) 22 .then(response => response.json()) 23 .then(data => { 24 loadingMessage.style.display = 'none'; 25 generateButton.style.display = 'block'; 26 27 if (data.error) { 28 alert(data.error); 29 } else { 30 const img = document.createElement('img'); 31 img.src = `data:image/png;base64,${data.image}`; 32 imageContainer.appendChild(img); 33 } 34 }) 35 .catch(error => { 36 loadingMessage.style.display = 'none'; 37 generateButton.style.display = 'block'; 38 alert('Error generating image. Please try again.'); 39 console.error('Error:', error); 40 }); 41}

Let's break down this function step by step.

Step 1: Capture User Input

First, we need to retrieve the values from the input fields where the user enters a prompt and selects an aspect ratio:

JavaScript
1const promptInput = document.getElementById('prompt').value; 2const aspectRatio = document.getElementById('aspect-ratio').value;

These values will be included in the request to our backend API.

Step 2: Get References to UI Elements

Next, we get references to the key elements in the UI that we'll need to interact with:

JavaScript
1const loadingMessage = document.getElementById('loading-message'); 2const generateButton = document.getElementById('generate-button'); 3const imageContainer = document.getElementById('image-container');

These elements will be used to show a loading state, hide the button, and display the generated image.

Step 3: Validate Input

Before proceeding, we check that the user has entered a prompt:

JavaScript
1if (!promptInput) { 2 alert('Please enter a prompt'); 3 return; 4}

This prevents the user from submitting an empty prompt and ensures meaningful input is sent to the backend.

Step 4: Update UI for Loading State

To indicate that the image is being generated, we update the UI accordingly:

JavaScript
1loadingMessage.style.display = 'block'; 2generateButton.style.display = 'none'; 3imageContainer.innerHTML = '';

This gives the user feedback and prepares the page to display a new image.

Step 5: Send Data to the Backend

We use the Fetch API to send the user's input to our Flask backend:

JavaScript
1fetch('/api/generate_image', { 2 method: 'POST', 3 headers: { 'Content-Type': 'application/json' }, 4 body: JSON.stringify({ user_input: promptInput, aspect_ratio: aspectRatio }) 5})

This makes a POST request to the /api/generate_image endpoint with the prompt and aspect ratio as JSON.

Step 6: Handle API Response

Once we receive a response from the backend, we parse the JSON and update the UI:

JavaScript
1.then(response => response.json()) 2.then(data => { 3 loadingMessage.style.display = 'none'; 4 generateButton.style.display = 'block'; 5 6 if (data.error) { 7 alert(data.error); 8 } else { 9 const img = document.createElement('img'); 10 img.src = `data:image/png;base64,${data.image}`; 11 imageContainer.appendChild(img); 12 } 13})

This code resets the UI, checks for errors, and displays the generated image if the request was successful.

Step 7: Catch Errors

Finally, we handle any unexpected issues that may occur during the request:

JavaScript
1.catch(error => { 2 loadingMessage.style.display = 'none'; 3 generateButton.style.display = 'block'; 4 alert('Error generating image. Please try again.'); 5 console.error('Error:', error); 6});

This ensures the user is informed and the UI returns to a usable state if something goes wrong.

Now, we're ready to send the request to our backend API. We'll explore this in more detail in the next section.

Working with the Fetch API

To send the user's input to our backend API, we'll use the Fetch API, which is a modern interface for making HTTP requests in JavaScript.

In our generateImage() function, we use fetch() to send a POST request to the /api/generate_image endpoint:

JavaScript
1fetch('/api/generate_image', { 2 method: 'POST', 3 headers: { 'Content-Type': 'application/json' }, 4 body: JSON.stringify({ user_input: promptInput, aspect_ratio: aspectRatio }) 5})

Let's break down this fetch request:

  • The first argument is the URL of the endpoint we're sending the request to: /api/generate_image.
  • The second argument is an object with options for the request:
    • method: 'POST' specifies that we're sending a POST request, which is appropriate for creating new resources (in this case, generating a new image).
    • headers: { 'Content-Type': 'application/json' } tells the server that we're sending JSON data.
    • body: JSON.stringify({ user_input: promptInput, aspect_ratio: aspectRatio }) converts our JavaScript object containing the user's input into a JSON string, which is the format our server expects.

The fetch() function returns a Promise that resolves to the Response object representing the response to the request. We can use the .then() method to handle the response:

JavaScript
1.then(response => response.json())

This line takes the Response object and calls its .json() method, which returns another Promise that resolves to the parsed JSON data from the response body.

We can chain another .then() to handle the parsed data:

JavaScript
1.then(data => { 2 loadingMessage.style.display = 'none'; 3 generateButton.style.display = 'block'; 4 5 if (data.error) { 6 alert(data.error); 7 } else { 8 const img = document.createElement('img'); 9 img.src = `data:image/png;base64,${data.image}`; 10 imageContainer.appendChild(img); 11 } 12})

This is where we process the response from our API and update the UI accordingly. We'll explore this in more detail in the next section.

Finally, we add a .catch() to handle any errors that might occur during the fetch operation:

JavaScript
1.catch(error => { 2 loadingMessage.style.display = 'none'; 3 generateButton.style.display = 'block'; 4 alert('Error generating image. Please try again.'); 5 console.error('Error:', error); 6});

If an error occurs, we reset the UI (hide the loading message, show the generate button), display an error message to the user, and log the error to the console for debugging purposes.

Now that we've sent the request to our API, let's see how we handle the response.

Handling API Responses

After sending the request to our backend API, we need to process the response and update our UI accordingly. This happens in the second .then() callback of our fetch operation:

JavaScript
1.then(data => { 2 loadingMessage.style.display = 'none'; 3 generateButton.style.display = 'block'; 4 5 if (data.error) { 6 alert(data.error); 7 } else { 8 const img = document.createElement('img'); 9 img.src = `data:image/png;base64,${data.image}`; 10 imageContainer.appendChild(img); 11 } 12})

First, we reset the UI to its normal state by hiding the loading message and showing the generate button:

JavaScript
1loadingMessage.style.display = 'none'; 2generateButton.style.display = 'block';

Then, we check if the response contains an error message:

JavaScript
1if (data.error) { 2 alert(data.error); 3}

If there's an error, we display it to the user using an alert.

If there's no error, we assume the response contains the generated image as a base64-encoded string:

JavaScript
1else { 2 const img = document.createElement('img'); 3 img.src = `data:image/png;base64,${data.image}`; 4 imageContainer.appendChild(img); 5}

Here, we:

  1. Create a new img element using document.createElement('img').
  2. Set its src attribute to a data URL that includes the base64-encoded image data from the response. The format data:image/png;base64,${data.image} tells the browser that this is a PNG image encoded in base64.
  3. Append the image element to the imageContainer using imageContainer.appendChild(img).

This will display the generated image on the page, allowing the user to see the result of their prompt.

It's worth noting that we're using a data URL to display the image directly, rather than loading it from a separate URL. This is a common approach for small images or when you want to avoid making additional HTTP requests.

With this code in place, our application can now generate images based on user input and display them on the page. The user can enter a prompt, select an aspect ratio and style, click the "Generate Image" button, and see the resulting image appear below the form.

Summary and Next Steps

Congratulations! In this lesson, we've implemented the client-side functionality for our image generation web application. Let's review what we've accomplished:

  1. We created a JavaScript file to handle the client-side logic of our application.
  2. We implemented the openTab() function to allow users to switch between tabs.
  3. We built the generateImage() function to capture user input, validate it, and send it to our backend API.
  4. We used the Fetch API to make asynchronous requests to our server.
  5. We processed the API responses and updated the UI to display the generated images.

Our application now has a functional user interface that allows users to generate images based on their input. However, there's still room for improvement. In the next lesson, we'll enhance our application by adding more robust loading states and error handling, as well as implementing the functionality to view previously generated images in the history tab.

In the practice exercises that follow this lesson, you'll have the opportunity to experiment with the JavaScript code we've written. You might try modifying the validation logic, adding additional features, or improving the error handling. These exercises will help reinforce your understanding of the concepts we've covered and give you hands-on experience with client-side JavaScript development.

Remember, JavaScript is a powerful language that can greatly enhance the user experience of your web applications. The skills you're learning in this course will be valuable not only for this specific project but for many other web development tasks you might encounter in the future.

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