- Timing
- CSS
- Section 1.1 - getFirstThreeFantasyBooks
- Section 1.2 - renderBookList
- Section 2.1 - getAuthor
- Section 2.2 - renderAuthor
- Section 2.3 - Adding it to our app
- Section 3.1 - createNewUser
- Section 3.2 - renderNewUserForm
- Section 3.3 - renderNewUser
- Section 3.4 - Adding it to our app
Hello everyone! Congratulations on making it to the end of our first 3 modules. These are really the "heart" of JavaScript, so if you feel good about it, you're going to be in great shape for the rest of the year. Mod 3 was all about asynchronous JavaScript and specifically using the fetch
api to get information from other places. Then, using that raw data to render something out to the screen for the user to see and interact with. You've done this all before, and that's all we're doing today, so no stress!
Now, even if you are stressed because this is a "big, scary week-long Lab," what should you do? What we tell you to do every morning: breathe. Let the adrenaline flow through you; your body is trying to help. So let's let it do its thing and pass by simply counting to 15. That may feel like ages, but remember those were seconds and you have days to get this done. Remember to come back to this whenever you're panicked about the time.
After you breathe and collect your senses, the next thing you should do is read over the entire README without coding a thing. This will help give you a lay of the land.
As we said, this Lab is about fetching and rendering. So, to that end, we recommend you do the short answers first, as they have useful information about the lab itself. Then, tackle each feature in the order outlined below. Each little feature builds on the last and uses the same pattern, which is:
- Build a fetch function to get the data
- Build a render function to put the data on the page
- Add it to your main program to see it work
- Adding any events
Your tests are also here to help you, and if you're ever confused about something, just focus on the tests! If you get them to pass, you're all set.
Remember, there are a lot of questions and code here. Do not spend too long on any one of them. Make sure to move. If you're still stuck exactly where you were after 5 minutes, take a break and try something else.
The two APIs we'll use today are:
You've used JSON Placeholder before, and we will use it to create a new user (it's one of the few free APIs that will let us make our own data). We will also use Open Library's API to get some books and authors. We selected this API because it's a little more "real world" with lots of information and quirks. We've kept it focused this time, but keep your wits about you!
Users should be able to do the following:
- Users can see a few fantasy books
- Users can click on a button for each book to view information about the author of that book
- Unrelated: Users can fill out a form to make a new user
Here's a final screenshot:
We added some very basic and open ended CSS. Please don't mess with it. This is not a CSS challenge, and we don't want you to waste a single second here. We've taken care of the basic styles (like divs
have borders so you can see things, and lists don't have their default styles).
And with that, off you go! Good luck!
In this section, we're going to grab three books, parse and format the data, and then render them to the screen.
Inside fetch-functions.js
you need to write a function getFirstThreeFantasyBooks
. This function takes in no arguments and returns 3 books of the fantasy genre. Here's the URL you should work with:
https://openlibrary.org/subjects/fantasy.json
Notice something about it: it ends with .json
. This is how this API deals with the different formats of data. It's an older convention. Normally, we'd use something like /api
, but here is a case where they chose not to! So don't forget to add that .json
to the end of your URL.
- It should send a fetch request to the correct URL (this is not a trick question; make sure to use the URL listed above)
- It should return a promise
- If there are no errors, the returned promise should resolve to an array of the first 3 books from the API
- See the Data Formatting section for details on how to format each book in the array.
- If there is a fetch error, catch the error.
console.warn
the error message, and the returned promise should resolve tonull
. - If the API response is not
ok
, then you should throw an error with the message of"Failed to get fantasy books"
- If you set up your function right, that means your function would
console.warn
the error message and then resolve tonull
- If you set up your function right, that means your function would
- We do mean "first 3" — we double-checked that URL, and there are definitely 3 books that meet all our needs. Please maintain the order so the tests pass (no sorting the books or anything)
The data you get back from the API is going to be a little messy. It's going to be an object with some metadata in it, but the part we want to deal with is the works
array, which holds our books. Here's an example response:
{
key: "/subjects/fantasy",
name: "fantasy",
subject_type: "subject",
work_count: 13234,
works: [
{ // the first "work"
authors: [
{
key: '/authors/OL22098A',
name: 'Lewis Carroll'
}
],
availability: { /* whatever don't care */ },
cover_edition_key: "OL31754751M",
cover_id: 10527843,
edition_count: 3557,
first_publish_year: 1865,
has_fulltext: true,
ia: "bwb_KS-001-699",
ia_collection: [ /* bunch more stuff */ ],
key: "/works/OL138052W",
lending_edition: "OL49950574M",
lending_identifier: "bwb_KS-001-699",
lendinglibrary: false,
printdisabled: true,
public_scan: true,
subject: [ /* Another giant array of things */ ],
title: "Alice's Adventures in Wonderland",
},
{
// Next work
}
// ... and so on
]
}
We are going to ignore pretty much all of that data(which is common when dealing with 3rd party APIs). Ultimately we want an array of 3 objects that each look like this:
[
{
title: "Alice's Adventures in Wonderland",
author: {
name: "Lewis Carroll",
urlKey: "/authors/OL22098A",
},
coverUrl: "https://covers.openlibrary.org/a/id/10527843-M.jpg"
},
// 2 more items
]
Here's the fun part: format the response to make it look like our data! Here's some help:
title
- This is just the
title
from the work object
- This is just the
author
- Books can have more than one author, but we don't care for now. So take the first author from the
authors
array and make an object with thename
andkey
properties. key
is being renamed tourlKey
to make it a little clearer what that value actually is. It's basically the author's ID plus the route "handle" (/authors
). We will use thisurlKey
later on in order to fetch that specific author's data.
- Books can have more than one author, but we don't care for now. So take the first author from the
coverUrl
- The url extension for the cover art.
- Our response gives us a
cover_id
in each work, but lets do some work upfront to convert the id to the image URL:https://covers.openlibrary.org/a/id/[THE_ID_HERE]-M.jpg
- For example, the id
13859660
corresponds to this image URL (test the URL in your browser!):https://covers.openlibrary.org/a/id/13859660-M.jpg
You can certainly test out your progress by running npm test
, however you should also test out your code by invoking your function.
In app.js
, you can see that we've imported your getFirstThreeFantasyBooks
function. As you see in app.js
there's already a decent amount of code written out. Do not remove any existing code (or change things like IDs) as the tests need these to run.
Towards the bottom of the app
function, look for the // Fetch the books!
comment. There you can invoke getFirstThreeFantasyBooks()
and console.log the returned set of books.
Run the program with npm run dev
and pop open your console to view your logs. If you see your array of 3 books, you should be good to go!
Alright, with that, you should be all set to move on!
Now that we have our data, we need to render it to the screen. You need to write a function renderBookList
in render-functions.js
, that takes in a bookListEl
(a ul
HTMLElement) and an array of books
. These are the books our fetcher function will return! This function will mutate the passed in bookListEl
and add a bunch of li
elements to it, one for each book.
- The function should remove any existing children from the passed in
bookListEl
- It should add a series of
li
elements to thebookListEl
for each book in thebooks
array - Each
li
should have (in this order)- an
img
tag with asrc
of thecoverUrl
and analt
ofAn old cover of [BOOK TITLE]
- a
p
tag with the textTitle: [BOOK TITLE]
- a
button
tag with the textView [AUTHOR NAME]
and adata-author-url-key
attribute with the value ofauthor.urlKey
- an
For example:
<li>
<img src="https://covers.openlibrary.org/a/id/13859660-M.jpg" alt="An old cover of Treasure Island">
<p>Title: Treasure Island</p>
<button data-author-url-key="/author/OL25963A">View Robert Louis Stevenson</button>
</li>
Don't worry about button functionality now, only rendering!
We've already created a bookListEl
variable for you to use. What you need to do here is use your fetch function getFirstThreeFantasyBooks
to get some books, and then invoke renderBookList
with the already created bookListEl
variable and your fetched books
to render your books to the screen!
You'll know you've done it if the tests are passing and the page loads up your books!
While your playing around, you may worry that your images won't load. That's fine, this is not exactly the fastest api in the world. It takes a bit for the images to load up. If they eventually show up (and if your tests are passing), you're all set! (This is a prime example of why you mock things in tests)
Do not call your fetching functions in your render functions! We want to keep our data layer and presentation layer separate.
Done with Feature 1! Nice job! Now, I know there are still those buttons, but those only come into play in the second feature!
As you know each book has an author, but in order to get any actual information about them we need to make another fetch. Every time we make that fetch, we're going to display the results in the author-info
div. So let's get to it!
Inside fetch-functions.js
we need to create the getAuthor
function. It will take in a urlKey
that we got from getThreeFantasyBooks
. That means this time we can't hardcode the url. Look at the url we want to hit:
https://openlibrary.org/authors/{SOME_ID}.json
Don't forget that .json
at the tail! But there's a minor wrinkle. In our books
objects, we have a urlKey
which is the author's ID plus the route handle (/authors
). That means when you're making your fetch function, you need to take into account that you aren't passing in OL22028A
but rather /authors/OL22028A
. How would you combine that with the host (https://openlibrary.org
) and the .json
to get the right URL?
Just like before, we'll need to so some formatting of the data as well.
- It should make a fetch to the correct, dynamic url
- It should return a promise
- If there are no errors, it should resolve a formatted
author
object (see Data Formatting below for more details) - If there is a fetch error it should resolve
null
andconsole.warn
the error message - If the API response is not
ok
, then you should throw an error with a message ofFailed to get author
- If you set up your function right, that means your function would
console.warn
the error message, and then returnnull
- If you set up your function right, that means your function would
The data you get back from the API is going to be a little messy again. Here's what we get:
{
"title": "[pseud.]",
"key": "/authors/OL22098A",
"death_date": "January 14, 1898",
"links": [ /* so much stuff */ ],
"type": { "key": "/type/author" },
"name": "Lewis Carroll",
"wikipedia": "http://en.wikipedia.org/wiki/Lewis_Carroll",
"personal_name": "Carroll, Lewis",
"bio": "Lewis Carroll is well known throughout the world as BLAH BLAAAHHHH BLAAAHHH MORE STUFF",
"alternate_names": [ /* so much stuff */ ],
"photos": [
6285807,
// possible other photo ids we don't care about after the first
],
"remote_ids": { /* so much stuff */ },
"location": "England",
"birth_date": "January 27, 1832",
"fuller_name": "Charles Lutwidge Dodgson",
"source_records": [ /* so much stuff */ ],
"latest_revision": 72,
"revision": 72,
"created": { /* so much stuff */ },
"last_modified": { /* so much stuff */ },
}
And what we want at the end is:
{
birthDate: 'January 27, 1832',
bio: 'Lewis Carroll is well known throughout the world as BLAH BLAAAHHHH BLAAAHHH MORE STUFF',
wikipediaLink: 'http://en.wikipedia.org/wiki/Lewis_Carroll',
name: 'Lewis Carroll',
pictureUrl: 'https://covers.openlibrary.org/a/id/6285807-M.jpg',
}
Now, we aren't going to give you hints this time. Make sure you get the final format right, the tests are specific! In the example above we used the exact data so you can make sure you mapped your new properties to the right API properties. Mind the pictureUrl
to make it correctly!
Just like before, we need to go into render-functions.js
and write a function renderAuthor
that takes in a div
element and an author
object and renders it to the screen. This function will mutate the passed in div
element. Here are the specific tags to add (in order and ONLY add these tags)
- An
h2
with the text[AUTHOR NAME]
- An
img
tag with asrc
of thepicture
and analt
ofA picture of [AUTHOR NAME]
- A
p
tag with the textBorn: [AUTHOR BIRTH DATE]
- A
p
tag with the text from thebio
property - An
a
tag with anhref
of thewikipediaUrl
property and the textWikipedia Link
And renderAuthor
should clear out the passed in div
of any existing children before adding these new ones. We're going to render every author
to the same div to keep things simple and we don't want them to accumulate.
Remember, each button in the books list has a button with a dataset attribute of the required author key. Your mission is to first attach a click listener to the list element with all our books, so that any time a button is clicked, we get the author data with getAuthour
and then render it to the author-info
div.
This action only has to fire when the user clicks the button, you don't need to load a default author or anything like that. Just make sure you're getting the right author for the right book!
Now we're going to say goodbye to our library API and use JSON Placeholder
to make a new user!
There's one function left in out fetch-functions.js
and that's createNewUser
. This will need to take in a user object as the single argument. It will look like this:
{
username: 'Zo_Bro',
isCool: true,
favoriteLanguage: 'JavaScript',
}
It will send it to the API to create a new user. We aren't going to tell you how to do this one. Only this:
- https://jsonplaceholder.typicode.com is the host
- JSON placeholder follows RESTful conventions
- You want to create a new
user
- If the request is successful, return the new user object that you get back
- This is just going to be a mirror of our data that we sent, but with an
id
property of11
added. It will always have an id of 11, that's ok! In our tests we can make it dynamic though, so don't just hardcode11
in your function.
- This is just going to be a mirror of our data that we sent, but with an
- If there is an error with the fetch, you should return
null
andconsole.warn
the error message - If the response is not
ok
then you should throw an error with the messageFailed to create new user
Unlike the other sections, we need some way for a user to enter information. You need to create a function renderNewUserForm
in render-functions.js
that takes in a form
element and renders the following HTML in it (that means mutate the passed in form!):
- A
label
with the textUsername
- An
input
with anid
ofusername
and aname
ofusername
- A
label
with the textIs this user cool?
- An
input
with anid
ofis-cool
and aname
ofisCool
and atype
ofcheckbox
- A
label
with the textFavorite Language
- A
select
with anid
offavorite-language
and aname
offavoriteLanguage
and the following options in this order. The list below is both the text and the value of the option:- None
- JavaScript
- Python
- Ruby
- A button with the text
Create User
to submit the form.
Do not attach any events here, just render the HTML. We'll attach the events later. The tests are exact, make sure you have these as we posted here.
Ok, and now we need to render the new user to the screen. You need to create a function renderNewUser
in render-functions.js
that takes in a div
element and a user
object and renders the following HTML to it:
- An
h2
with the text of theusername
property- The
h2
must also have a dataset attribute ofuserId
with the value of theid
property
- The
- A
p
with the textThe hippest in the house!
if theisCool
property istrue
andA real square.
if it'sfalse
- Another
p
with the textFavorite Language: [FAVORITE LANGUAGE]
Just like the other render functions, this function should remove any child elements form the passed in div
element to avoid repeating any tags.
Alright, now to add this to our app there are a few things we need to do:
- Render the form
- Attach an event listener to the form to handle submits
- The submission handler should call
createNewUser
with the form data (careful with that checkbox value! It's not a boolean by default...) - After creating the user, call
renderNewUser
with thenewUserDiv
and the new user object
That's all you need to do! If you've done it all correctly, you should have a working app that looks like the screenshot at the top of this README. If you're having trouble, remember to breathe, and then read the tests. They're there to help you!