The Making of Ghibli Watch
Donte Ladatto
Posted on April 7, 2022
About the App
Ghibli Watch is my first attempt at creating a single page application centered around Javascript events, DOM manipulation, and server communication. My significant other is a big Studio Ghibli fan, and I've really enjoyed the few films I've seen as well. The beautiful hand-drawn animation and oft fantastical storytelling pair well to provide an enchanting viewing experience, and I'm here for more! Thus, I set out to create an application that would allow myself and others like me to discover and compile lists of Studio Ghibli films (with the help of this wonderful API). Below I will detail the problems I encountered, along with the steps I took to solve them. I do this not only to further my own understanding, but to also help others who might find themselves in similar situations down the road. Now, without further ado...
How do I take the data I GET from the API and POST it to a local database?
This is the first big question I had during the building process. I knew how to fetch the data and append it to the page, but for users to be able to add the selected film to a list and have it persist, I needed to be able to send that same data to another database (in this case, a mock back-end created using JSON Server). Being relatively new to this sort of thing, this initially seemed like a daunting prospect. After couple of moments of consideration, the simple solution materialized; save the keys from the dataset as variables to use in the body of my 'POST' configuration object and add the event listener for the buttons all within the same function.
function displayFilm(e) {
fetch(BASE_URL + `/${e.target.id}`)
.then(res => res.json())
.then(data => {
let image = data.movie_banner
let title = data.title
let year = data.release_date
let desc = data.description
display.innerHTML =
`<img class="resize" src="${image}">
<h1>${title} <span class="year">${year}</span></h1>
<p>${desc}</p>
<button class="watch">Add to Watchlist</button>
<button class="favorite">Add to Favorites</button>`
display.addEventListener('click', (e) => {
let endpoint;
if (e.target.className === "watch") {
endpoint = "/watchlist"
}
else if (e.target.className === "favorite") {
endpoint = "/favorites"
}
fetch(LOCAL_URL + endpoint, {
method: 'POST',
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify({
"movie_banner": image,
"title": title,
"release_date": year,
"description": desc
})
})
}
})
)
})
}
This worked like a charm, or so it appeared...
Why am I sending multiple POST requests with one click?
This one's admittedly a little silly looking back, but it was a real headache for me for a while. I had added my event listener like so:
//Defined globally
const display = document.querySelector("#show-panel")
//Inside film rendering function
display.addEventListener('click', (e) => {...}
The problem with this was that the listener was attached to an existing HTML element, instead of one created by my film rendering function. So while display.innerHTML = ""
would reset the display window every time a film was rendered, the event listeners would stick around. This resulted in unwanted post requests being made from presumably clicking one button.
I corrected this by adding an event listener to each button as they were created, but I could also have added a nested div to the rendered HTML and attached my singular listener to it instead.
const addWatch = display.querySelector(`.watch`)
const addFav = display.querySelector(`.favorite`)
const postConfig = {
method: 'POST',
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
"movie_banner": image,
"title": title,
"release_date": year,
"description": desc
})
}
addWatch.addEventListener('click', (e) => {
fetch(LOCAL_URL + '/watchlist', postConfig)
.then(res => res.json())
})
addFav.addEventListener('click', (e) => {
fetch(LOCAL_URL + '/favorites', postConfig)
.then(res => res.json())
})
How do I keep duplicate data from being posted?
One of my main concerns going into this project was how I was going to keep films from being added to the same list twice. My first thought was to disable the buttons on click...
addWatch.addEventListener('click', (e) => {
fetch(LOCAL_URL + '/watchlist', postConfig)
.then(res => res.json())
e.target.disabled = true
})
... but the film could still be added again the next time it was rendered. No, the answer was to assign an ID to each film inside of the configuration object. So:
//I assign the ID key from the API to a variable...
let eyeD = data.id
//... then add this new variable to my POST configuration object
"id": eyeD
Voila! Now a duplicate POST request throws a 500 (Internal Server Error) in the console. Now to alert the user...
//Each one following its respective fetch:
.catch(error => alert(`${title} is already on your watchlist!`))
.catch(error => alert(`${title} is already in your favorites!`))
And now, a quandary born of aesthetics...
Behold, the list view when there is data to be rendered:
But when the fetch comes up empty:
Blech. I wasn't a fan of how that looked. Let's take a peek at the list rendering function:
function displayList(e) {
display.innerHTML = ""
let endpoint;
let header;
if (e.target.id === "watchlist") {
endpoint = "/watchlist"
header = "My Watchlist"
}
else if (e.target.id === "favslist") {
endpoint = "/favorites"
header = "My Favorites"
}
display.innerHTML = `<h2>${header}</h2>`
fetch(LOCAL_URL + endpoint)
.then(res => res.json())
.then(data => data.forEach(film => {
display.innerHTML += `<div class="card"><img id="${film.id}" class="thumbnail" src="${film.movie_banner}"><br>
<h3>${film.title}</h3><button class="delete" id="${endpoint + "/" + film.id}">Remove from List</button><br><br></div>`
}))
}
As we can see, the function makes a GET request to the local server with an endpoint determined by the ID of the button that was clicked, and then appends data from the returned promise object to the DOM. But what if there isn't any data? Apologies to Taylor Swift, but I don't want to see any blank space.
How can I display a message when a GET request returns an empty promise object?
Right off the bat, I knew that any change I was going to make would have to be within my fetch. Number two, I knew that I wanted to add new HTML if there was no data to display. Ah-ha, an if statement! But how do I write out my condition? Taking a look at my db.json file, I see this: "watchlist": [],
. It's an empty array.
fetch(LOCAL_URL + endpoint)
.then(res => res.json())
.then(data => {
if (data.length === 0) {
display.innerHTML += `<p class="empty">You haven't added anything to this list yet; select a film and get started!</p>`
}
else {
data.forEach(film => {
display.innerHTML += `<div class="card"><img id="${film.id}" class="thumbnail" src="${film.movie_banner}"><br>
<h3>${film.title}</h3><button class="delete" id="${endpoint + "/" + film.id}">Remove from List</button><br><br></div>`
})
}
})
Boom, if (data.length === 0)
. But if I were to remove every film from the list, we're back at square one with the blank space (until another user input resets the display). So I also needed to add to my delete functionality:
document.addEventListener("click", (e) => {
if (e.target.className === "delete") {
fetch(LOCAL_URL + `${e.target.id}`, {
method: "DELETE"
})
.then(res => res.json())
.then(res => {
e.target.parentElement.remove()
})
}
})
I knew I would be writing another if statement, but I couldn't use the same method I did with the GET request because DELETE doesn't return anything but a status code (I just type .then(res => res.json())
to reinforce the habit). But in this scenario, the ugly blank space is a direct result of me removing elements from the DOM. So I simply need to tack on a .then()
with an if statement that checks if a certain element is present or not...
.then(res => {
if (document.querySelector(".card") === null) {
display.innerHTML += `<p class="empty">You haven't added anything to this list yet; select a film and get started!</p>`
}
})
When will I see that blank space again? "Quoth the Raven..." etc.
It was a small change, but a necessary one.
In Conclusion
This project, though small in size, afforded me plenty of learning opportunities through research, trial and error. I hope to use this experience in continuing to build grander works in the near future. I also hope that if nothing else you were able to laugh at my follies. 'Til next time!
Posted on April 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.