- Introduction
- Code of Conduct
- Resources
- Homework
- Homework - week one and two
- Summary of Tools and Technology
- Today's Exercise
- The Command Line
- Examine the Starter Page
- CSS Enhancements
- Aside: CSS targeting with Page Fragments
- DOM Scripting Review
- Looping - for and forEach()
- EXERCISE I - Generating Content From an Array
- EXERCISE II Content Generation with an Array of Objects
- Array Methods
- EXERCISE III - Using Array.prototype.map()
- Responsive Navigation Bug
- APIs: Local Storage and Fetch
- Adding Content
- News Navigation
- There is no such thing as a silly question, you are encouraged to speak up anytime something is not clear to you
- There is no such thing as a silly mistake, they are a gateway to learning
- Do not dismiss someone because they have a different level of experience - be kind to others
- During class exercises you promise to alert me the second something goes awry with your project
- I will make myself available after class to clarify or expand on topics (or we can set an alternate time via email)
Your instructor - Daniel Deverell (he, him): Front & Back end developer at JPMorgan Chase specializing in data visualization, React and Node.
- 6:30 PM - 9:30 PM Tuesdays and Thursdays
- Daniel Deverell, email -
daniel.deverell@nyu.edu
- Syllabus
- Office hours will be held an an as needed basis. Please email me if you would like to make an appointment.
Github - https://github.com/front-end-intermediate
, is the source for all files used in this class. Each class's activities will be documented in a readme file.
You can download a zip file from Github using the green "Clone or Download" menu and selecting "Download ZIP."
Please keep the sessions home page open in a tab during class for reference and in order to copy and paste code.
The edited files as they existed at the end of class can be downloaded from the *-done
branch of this and all subsequent Github repositories. Be sure to select the branch on Github before downloading the zip. I will demonstrate this in class.
Online reading and videos will be assigned.
Homework will be handed in via Github and then alerting me via email - daniel.deverell@nyu.edu. I can schedule special sessions for those who require assistance setting up Git and Github.
If necessary you should read MDN on Basic DOM Manipulation and do the exercise.
Read Fetching Data from the Server and do the exercise.
Mozilla Develpers Network offers a series of JavaScript tutorials.
For our current purposes, special attention should be paid to the following JavaScript tutorials on MDN:
A listing of applications and technologies you will be introduced to in the class include:
- Intermediate/advanced HTML, CSS and JavaScript
- React, NODE, MongoDB, ExpressJS
- Visual Studio Code and the Terminal
- Git and Github
- Styled Components (CSS in JS)
In today's class we will implement this single page web site with content almost entirely generated using JavaScript (try selecting view > developer > View Source
in Chrome).
In creating this page we will focus on techniques that are critical, not just for working effectively with DOM manipulation, but for working with React and other JavaScript frameworks.
In this class we will be using Visual Studio Code as our editor. We will discuss its features on an as-needed basis.
Start VSCode, press cmd + shift + p
and type in the word shell
. Select Install code command in PATH
.
- Live Server
- Prettier - edit the settings in VS Code to enable format on save
e.g.:
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
Note: you can control your setting for prettier by adding a configuration file called .prettierrc
at the top level of your project, i.e.:
{
"singleQuote": true,
"trailingComma": "all"
}
A complete list is located here. Here is a fuller set:
{
"singleQuote": true,
"trailingComma": "all",
"semi": true,
"tabWidth": 4,
"arrowParens": "avoid"
}
You are going to need to have a minimal set of terminal commands at your disposal.
(Windows users can use the Git Bash terminal that is installed along with Git.)
Start the terminal app:
$ cd // change directory
$ cd ~ // go to your home directory
$ cd <PATH> // Mac: copy and paste the folder you want to go to
$ cd Desk // tab completion
$ cd .. // go up one level
$ ls // list files, dir on a PC
$ ls -al // list file with flags that expand the command
$ pwd // print working directory
cd
into today's working directory - and type:
code .
Open index.html
, right or control click on it and choose 'Open with Live Server'.
Note the responsive navigation on small screen and the response to changing the OS between light and dark mode.
Examine the CSS file. Note the use of viewport sized typography for the h1
.
We are using a common responsive hamburger menu technique.
We are currenly viewing a single page application - there is only one HTML file and we scroll up and down to view the content.
Aside: "All the News That Fits We Print!" is an old joke. "All the News That's Fit to Print," is the actual slogan of The New York Times.
As an enhancement, we will add settings for light and dark modes using prefers-color-scheme.
The entire updated CSS file is available in the "other folder" of this repository as NEW-styles.css
. Copy and paste the contents into styles.css
and view the changes via the Source Control diff.
We begin by declaring CSS variables for color in a prefers-color-scheme media query.
@media (prefers-color-scheme: dark) {
* {
--textcolor: #eee;
--bgcolor: #555;
--bgcolor-darker: #333;
--highlight: #ffc400;
}
}
@media (prefers-color-scheme: light) {
* {
--textcolor: #333;
--bgcolor: #fff;
--bgcolor-darker: #ddd;
--highlight: #ffc400;
}
}
We then use these variables throughout the CSS file e.g.:
body {
color: var(--textcolor);
background: var(--bgcolor-darker);
}
.site-wrap {
background: var(--bgcolor);
}
html {
box-sizing: border-box;
background: var(--bgcolor-darker);
}
Use the system preferences (Appearance on a Mac) to test.
Experiment by adding the following to make only one section visible at a time:
/* hides all the sections */
section {
display: none;
}
/* shows only one selection based on the location hash */
section:target {
display: block;
}
Note the section:target selector. See a demo here.
We will use a single page scrolling app. Comment out the two CSS rules above and add:
html {
scroll-behavior: smooth;
}
Add jump links to the top of the page in each section:
<!-- an id for the top -->
<body id="top">
...
<!-- before the close of each of the sections -->
<!-- USE VS Code's multiple selections feature - Command-d on a Mac -->
<a href="#top">Back to top</a>
...
</body>
In order to select and style the new links we could add a class, but in this case it is easier to use an attribute selector:
a[href="#top"] {
display: block;
padding-bottom: 2rem;
color: var(--highlight);
}
Note: The navigation scrolls off the screen and we want to to be available at all times.
We will anchor the menu to the top of the screen once the user has scrolled to the point where the menu would normally be out of sight using the css position property.
Edit the CSS in nav.css
(inside the media query).
.main-menu {
...
position: sticky;
top: 0px;
}
The HTML DOM (Document Object Model) specification allows JavaScript to access and manipulate the elements of an HTML document.
Use document.querySelectorAll('selector')
to find all matching elements on a page. You can use any valid CSS for the selector.
In the browser's console:
var elems = document.querySelectorAll(".main-menu li");
Returns a NodeList.
Use document.querySelector()
(without the 'All') to find the first matching element on a page.
In the browser's console:
var elem = document.querySelector(".main-menu li");
Returns the first HTML element or "Node" matched by the selector
We can use a for
to loop through any iterable object - including arrays and node lists.
var elems = document.querySelectorAll(".main-menu a");
for (let i = 0; i < elems.length; i++) {
console.log('index: ', i);
console.log(`elems[${i}].href: `, elems[i].href);
}
ES6 introduced the forEach()
method for looping over arrays. We use forEach when we want to perform some sort of action on every item in an array.
The forEach method accepts a function as its argument. This is commonly known as a callback function. The term “callback function” refers to a function that we pass to another function.
The first argument in the callback is the current item in the loop. The second (optional) is the current index in the array.
var elems = document.querySelectorAll(".main-menu a");
elems.forEach(function (item, index) {
console.log('index: ', index);
console.log('item.href: ', item.href);
});
We will begin by replacing the existing nav with items from an array using a for loop
.
Note the two script tags at the bottom of index.html
:
<script src="js/modules/navitems.js"></script>
<script src="js/index.js"></script>
Examine navitems.js
.
Note the difference between navItemsObject
and navItemsArray
. The latter is an array of values while the former offers an array of objects consisting of key/value pairs.
In the browser console:
navItemsArray;
navItemsObject;
Compare elems
( var elems = document.querySelectorAll(".main-menu a")
) and navItemsArray
and note the prototypes
in the inspector. The first is a NodeList and the latter an Array.
Both have a length property - elems.length
and navItemsArray.length
but the methods / functionality are different.
In index.js
, select the HTML element with the class .main-menu
In index.js
:
const nav = document.querySelector(".main-menu");
To select all the links in nav we could write:
const navList = document.querySelectorAll("#main-menu li a");
But here it is perhaps a bit more efficient to use element.querySelector
(as opposed to document.querySelector
):
const nav = document.querySelector(".main-menu");
const navList = nav.querySelectorAll("li a");
Replace our placeholder nav items with content from an array
- use a
for
loop andinnerHTML
:
const nav = document.querySelector(".main-menu");
const navList = nav.querySelectorAll("li a");
for (let i = 0; i < navList.length; i++) {
console.log(i);
navList[i].innerHTML = navItemsArray[i];
}
We know that the text being displayed in the UI is coming from the navItemsArray because they are now capitalized.
So we will dynamically generate the nav from items in the array.
Append a <ul>
tag to nav using:
- document.createElement(): creates an element, e.g.
var div = document.createElement('div');
. - append.
JavaScript offers a number of methods to determine the insertion point.
In the console one at a time:
// Create a new HTML element and add some text
var elem = document.createElement("div");
elem.textContent = "I'm inserted via DOM scripting";
// Get the element to add your new HTML element before, after, or within
var target = document.querySelector(".main-menu");
// Inject the `div` element before the element
target.before(elem);
// Inject the `div` element after the element
target.after(elem);
// Inject the `div` element before the first item *inside* the element
target.prepend(elem);
// Inject the `div` element after the first item *inside* the element
target.append(elem);
Let's append a new div to the (now empty) nav.
Delete eveything in index.js
and add:
const nav = document.querySelector(".main-menu");
const navList = document.createElement("ul");
navList.textContent = "Hello world";
nav.append(navList);
Note the new <ul>
in the header.
Dynamically create the nav based on the number of items in the array using a for loop:
const nav = document.querySelector(".main-menu");
const navList = document.createElement("ul");
for (let i = 0; i < navItemsArray.length; i++) {
let listItem = document.createElement("li");
let linkText = navItemsArray[i];
listItem.innerHTML = '<a href="#">' + linkText + "</a>";
navList.append(listItem);
}
nav.append(navList);
Note the use of quotes in the construction or concatination of our innerHTML: listItem.innerHTML = '<a href="#">' + linkText + "</a>"
. We also used the addition operator (+).
Edit the HTML to remove the navigation links:
<nav id="main-menu" class="main-menu" aria-label="Main menu">
<a
href="#main-menu-toggle"
id="main-menu-close"
class="menu-close"
aria-label="Close main menu"
>
<span class="sr-only">Close main menu</span>
<span class="fa fa-close" aria-hidden="true"></span>
</a>
<!-- HERE -->
</nav>
Our nav bar now displays all the items in our array but the code is a bit ugly. This is an example of imperative programming. In order to prepare for React we will adopt a more declarative style.
We will use Functional Programming techniques.
First, switch out the old school concatenation for a template string:
for (let i = 0; i < navItemsArray.length; i++) {
let listItem = document.createElement("li");
listItem.innerHTML = `<a href="#">${navItemsArray[i]}</a>`;
navList.append(listItem);
}
Use forEach()
instead of a for loop:
navItemsArray.forEach(function (item) {
let listItem = document.createElement("li");
listItem.innerHTML = `<a href="#">${item}</a>`;
navList.appendChild(listItem);
});
Use an arrow function in the final script:
navItemsArray.forEach((item) => {
let listItem = document.createElement("li");
listItem.innerHTML = `<a href="#">${item}</a>`;
navList.appendChild(listItem);
});
Template Literals, aka Template Strings use back ticks instead of quotes and use plaeholders - ${ ... }
instead of +
signs for JS expressions.
Compare old school concatenation by running the following in the console:
const name = "Yorik";
const age = 2;
const oldschool = "My dog " + name + " is " + age * 7 + "years old.";
const newschool = `My dog ${name} is ${age * 7} years old.`;
console.log("oldschool::", oldschool);
console.log("newschool::", newschool);
In order to use string interpolation, we use backticks (`). The key is found in the top-left corner, above “Tab”. It shares a key with the tilde (~) character.
We create a dynamic segment within our string by writing ${}
. Anything placed between the squiggly brackets will be evaluated as a JavaScript expression.
Arrow functions are commonly used as a shorter syntax for anonymous functions.
Historically, functions in JavaScript have been written using the function keyword:
function exclaim(string) {
return string + "!";
}
The equivalent arrow function looks like this:
const exclaim = (string) => string + "!";
There are two types of arrow functions: short and long form.
Short form:
const add1 = (n) => n + 1;
Long form:
const add1 = (n) => {
return n + 1;
};
The short-form function's body must be a single expression while the long-form function's body can contain one or more statements.
When we add the curly braces and convert to the long-form, our function can now execute multiple instructions. For example, we can use the long-form if we want to perform a check before the main bit of logic:
const add1 = (n) => {
if (typeof n !== "number") {
throw new Error("Argument provided must be a number");
}
return n + 1;
};
The parentheses are optional:
// This is valid:
const logUser = user => {
console.log(user);
}
// This is also valid:
const logUser = (user) => {
console.log(user);
}
Parentheses are mandatory if we have more than 1 parameter:
const updateUser = (user, properties, isAdmin) => {
if (!isAdmin) {
throw new Error("Not authorized");
}
user.setProperties(properties);
};
updateUser("Daniel", { id: 90 }, false);
The parentheses are also mandatory if we have no parameters:
const sayHello = () => console.log("Hello!");
So far we have been working with a simple array. However most of the data you will encounter will consist of an array of objects Examples:
- JSON Placeholder, documentation
- City Growth, documentation
- Pokemon API, documentation
- and our navItemsObject:
const navItemsObject = [
{
label: "Arts",
link: "#arts",
},
{
label: "Books",
link: "#books",
},
{
label: "Fashion",
link: "#fashion",
},
{
label: "Food",
link: "#food",
},
{
label: "Movies",
link: "#movies",
},
{
label: "Travel",
link: "#travel",
},
];
Add the links using navItemsObject
instead of navItemsArray
.
Note the the 'dot' accessor notation (item.link
) for accessing a value from an object (and the addition of the anchor tags):
navItemsObject.forEach(function (item) {
let listItem = document.createElement("li");
listItem.innerHTML = `<a href="${item.link}">${item.label}</a>`;
navList.appendChild(listItem);
});
Navigate and inspect the code and note that we now have anchor tags with page fragment links in our html and are able to navigate within our page.
JavaScript gives us several tools for iterating over the items in an array. We could have used a for loop, and it's arguably much simpler. So why should we learn about .forEach
?
forEach
is part of a family of array looping or "iteration" methods. Taken as a whole, this family allows us to do all sorts of things, like finding a particular item in the array, filtering an Array, and much more.
All of the methods in this family follow the same basic structure. For example, they all support the optional index parameter.
Let's look at another member of this family: the filter method.
Note the inventors sample data in navitems.js
:
const inventors = [
{ first: "Albert", last: "Einstein", year: 1879, passed: 1955 },
{ first: "Isaac", last: "Newton", year: 1643, passed: 1727 },
{ first: "Galileo", last: "Galilei", year: 1564, passed: 1642 },
{ first: "Marie", last: "Curie", year: 1867, passed: 1934 },
{ first: "Johannes", last: "Kepler", year: 1571, passed: 1630 },
{ first: "Nicolaus", last: "Copernicus", year: 1473, passed: 1543 },
{ first: "Max", last: "Planck", year: 1858, passed: 1947 },
];
Filter the list of inventors for those who were born in the 1500's.
In the console:
const fifteen = inventors.filter(
function(inventor) {
return inventor.year >= 1500 && inventor.year < 1600
}
);
console.table(fifteen);
Note the use of the &&
operator.
We typically see this with an arrow function (with implicit return):
const fifteen = inventors.filter(
(inventor) => inventor.year >= 1500 && inventor.year < 1600
);
console.table(fifteen);
Note the lack of a return
statement. While they can be used within arrow functions, they are often unnecessary as the return
is implicit.
Another filter example:
const students = [
{ name: "Aisha", grade: 89 },
{ name: "Bruno", grade: 55 },
{ name: "Carlos", grade: 68 },
{ name: "Dacian", grade: 71 },
{ name: "Esther", grade: 40 },
];
const studentsWhoPassed = students.filter((student) => {
return student.grade >= 60;
});
console.log(studentsWhoPassed);
In many ways, filter is very similar to forEach. It takes a callback function, and that callback function will be called once per item in the array.
Unlike forEach, however, filter produces a value. Specifically, it produces or "returns" a new array which contains a subset of items from the original array.
The callback function inside filter should return a boolean value, either true or false. The filter method calls this function once for every item in the array. If the callback returns true, this item is included in the new array. Otherwise, it's excluded.
Note: The filter method doesn't modify or mutate the original array. This is not true for all of the array methods and it is an important distinction.
const nums = [5, 12, 15, 31, 40];
const evenNums = nums.filter((num) => {
return num % 2 === 0;
});
console.log(nums); // Hasn't changed: [5, 12, 15, 31, 40]
console.log(evenNums); // [12, 40]
nums.pop();
console.log(nums); // Changed: [5, 12, 15, 31]
map
is similar to forEach. We provide a callback function, and it iterates over the array, calling the function once for each item in the array.
Here's the big difference, though: map produces a brand new array, full of transformed values.
The forEach function will always return undefined:
const nums = [1, 2, 3];
const result = nums.forEach(num => num + 1);
console.log(result); // undefined
In contrast, map will "collect" all the values we return from our callback, and put them into a new array:
const nums = [1, 2, 3];
const result = nums.map((num) => num + 1);
console.log(result); // [2, 3, 4]
Provide an array of the inventors first and last names:
var fullNames = inventors.map(function (inventor) {
return `${inventor.first} ${inventor.last}`;
});
console.log("Full names: " + fullNames);
Notice the commas separating the names.
Refactor it to use an arrow function and join the results with a slash:
const fullNames = inventors
.map((inventor) => `${inventor.first} ${inventor.last}`)
.join(" / ");
console.log("Full names: ", fullNames);
Array.map is frequently used to generate html:
.map((inventor) => `<li>${inventor.first}, ${inventor.last}</li>`)
Sort the inventors by birthdate, oldest to youngest:
const ordered = inventors.sort(function (a, b) {
if (a.year > b.year) {
return 1;
} else {
return -1;
}
});
console.table(ordered);
How many years did all the inventors live?
const totalYears = inventors.reduce((total, inventor) => {
return total + (inventor.passed - inventor.year);
}, 0);
These declarative as opposed to imperative methods, .map
, .filter
, reduce
, sort
and others are the prefered means of working with data - and are especially important for the effective use of React.
Let's create the list items using Array.map()
and template strings:
const nav = document.querySelector(".main-menu");
const markup = `
<ul>
${navItemsObject.map(function (item) {
return `<li><a href="${item.link}">${item.label}</a></li>`;
})}
</ul>
`;
console.log(markup);
nav.innerHTML = markup;
Note the use of nested template strings here. We are using a template string to create the markup for the list items, and then we are using another template string to create the markup for the entire list.
Join the array to avoid the comma:
const markup = `
<ul>
${navItemsObject
.map(function (item) {
return `<li><a href="${item.link}">${item.label}</a></li>`;
})
.join("")}
</ul>
`;
Finally, refactor using an arrow function:
const markup = `
<ul>
${navItemsObject
.map( item => `<li><a href="${item.link}">${item.label}</a></li>`)
.join("")}
</ul>
`;
nav.append(navList)
is nownav.innerHTML = markup
since we no longer usedocument.createElement()
.
Examine the responsive navigation on small screens using the device toolbar. The close button has been lost due to the use of nav.innerHTML = markup
. We essentially blew out all the html and replaced it with our own - the anchor tag is missing.
There are a number of ways to resolve this.
One might be to just cut the anchor tag from index.html
and paste it into our JavaScript:
const markup = `
<ul>
<a
href="#main-menu-toggle"
id="main-menu-close"
class="menu-close"
aria-label="Close main menu"
>
<span class="sr-only">Close main menu</span>
<span class="fa fa-close" aria-hidden="true"></span>
</a>
${navItemsObject
.map((item) => `<li><a href="${item.link}">${item.label}</a></li>`)
.join("")}
</ul>
`;
We will leave the code in the HTML and add an element to the DOM to receive our generated navigational HTML:
<nav id="main-menu" class="main-menu" aria-label="Main menu">
<a
href="#main-menu-toggle"
id="main-menu-close"
class="menu-close"
aria-label="Close main menu"
>
<span class="sr-only">Close main menu</span>
<span class="fa fa-close" aria-hidden="true"></span>
</a>
<!-- ADD HERE before the close of </nav> -->
<span id="main-nav"></span>
</nav>
And add the markup to it:
nav.querySelector("#main-nav").innerHTML = markup;
Add type="module"
to the script tag in index.html
:
<script type="module" src="js/index.js"></script>
Create app/js/modules/nav.js
and move the code from index.js
to it in an exported function:
export function makeNav() {
const nav = document.querySelector(".main-menu");
const markup = `
<ul>
${navItemsObject
.map(
(item, index) =>
`<li class="navitem-${index}"><a href="${item.link}">${item.label}</a></li>`
)
.join("")}
</ul>
`;
nav.querySelector("#main-nav").innerHTML = markup;
}
Import the function in index.js
and call it:
import { makeNav } from "./modules/nav.js";
makeNav();
We should export and import the navItemsObject
as well.
Remove <script src="js/modules/navitems.js"></script>
from index.html
In navitems.js at the bottom add:
export default navItemsObject;
In modules/nav.js
at the top:
import navItemsObject from "./navitems.js";
We are using the import
and export
statements. These are part of the ES6 module system. Note the difference between export default
and export
.
An API - Application Programming Interface - is a set of definitions, communication protocols, and tools for building web sites. In general terms, it is a set of clearly defined methods of communication among various components. A good API makes it easier to develop by providing building blocks for us to use.
We covered the browser APIs fetch and local storage in the previous session. We will review them now.
We will use the NY Times developer API for getting a data using my API key.
Note there is a limit of 10,000 requests per day or 10 requests per minute. We will work around this by storing the data locally in the browser's local storage.
Instead of requesting the data from the NY Times we will request it from our own browsers.
The specific API endpoint for this is their top stories endpoint. It lets us request the top stories from a specific section of their publication.
Start by removing the existing HTML content (all the sections and the site wrap div) from the site-wrap div in index.html
and add some boilerplate:
<main class="site-wrap">
<h2>Welcome!</h2>
<p>
All the news you need for your leisure activities courtesy of the New
York Times.
</p>
<p>To get started make a selection above.</p>
</main>
Store the API key, a template string with the complete URL, and the element we want to manipulate (.site-wrap
) in a variable:
const root = document.querySelector(".site-wrap");
const nytapi = "KgGi6DjX1FRV8AlFewvDqQ8IYFGzAcHM"; // note this should be your API key
const nytUrl = `https://api.nytimes.com/svc/topstories/v2/travel.json?api-key=${nytapi}`;
Web browsers offer a large number of built-in web APIs.
We'll use the Fetch API to get data from the New York Times.
fetch()
doesn't return the data immediately, rather it returns a promise that the data will eventually become available.
fetch(nytUrl).then(function (response) {
console.log("Response ::: ", response);
});
The response needs to be converted to JSON with response.json()
.
Note that despite the method being named json()
, the result is not JSON but is instead the result of taking JSON as input and parsing it to produce a JavaScript object.
We can then use the data in our app:
fetch(nytUrl)
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(myJson);
});
Most prefer to use arrow functions (for obvious reasons):
fetch(nytUrl)
.then((response) => response.json())
.then((myJson) => console.log(myJson));
Try console.log(myJson.results)
Instead of logging it we will pass it to a renderStories
function:
fetch(nytUrl)
.then((response) => response.json())
// storing the data in localstorage to avoid hitting the API limit
.then((myJson) => localStorage.setItem("stories", JSON.stringify(myJson)))
.then(renderStories);
Examine the data and you'll see that the information we are interested in is located in results
.
We will use a forEach loop to log each of the results:
In renderStories
we take the data from local storage, convert it to json and run a forEach
on every item that logs it to the console:
function renderStories() {
let data = JSON.parse(localStorage.getItem("stories"));
data.results.forEach(function (story) {
console.log(story);
});
}
Let's use the techniques we covered earlier to create a DOM element for each of the stories:
function renderStories() {
let data = JSON.parse(localStorage.getItem("stories"));
data.results.forEach( (story) => {
let storyEl = document.createElement("div");
storyEl.className = "entry";
storyEl.innerHTML = `
<h3>${story.title}</h3>
`;
console.log(storyEl);
root.prepend(storyEl);
});
}
Expand it to include images and abstracts:
function renderStories() {
let data = JSON.parse(localStorage.getItem("stories"));
data.results.forEach((story) => {
let storyEl = document.createElement("div");
storyEl.className = "entry";
storyEl.innerHTML = `
<img src="${story.multimedia[0].url}" alt="${story.title}" />
<div>
<h3><a target="_blank" href="${story.url}">${story.title}</a></h3>
<p>${story.abstract}</p>
</div>
`;
root.prepend(storyEl);
});
}
Note: not all NYTimes stories include images and our script could error if story.multimedia[0]
was undefined. For this we will use a Conditional (ternary) operator for the image element:
<img src="${story.multimedia ? story.multimedia[0].url : ""}" alt="${story.title}" />
Ternaries are popular in cases like this - in fact they are essential. You cannot write an if(){} else(){}
statement inside a string literal. Template literals only support expressions.
Add some new css to support the new elements into the break point CSS so it only applies to the wide screen view:
.entry {
display: grid;
grid-template-columns: 2fr 6fr;
grid-column-gap: 1rem;
margin-bottom: 1rem;
}
.entry a {
text-decoration: none;
color: var(--textcolor);
font-size: 1.5rem;
}
Note: examine the json and try incrementing the [0]
in the ternary to get a better image.
Refactor using arrow functions and .map()
:
function renderStories() {
let data = JSON.parse(localStorage.getItem("stories"));
data.results.map((story) => {
var storyEl = document.createElement("div");
storyEl.className = "entry";
storyEl.innerHTML = `
<img src="${story.multimedia ? story.multimedia[0].url : ""}" alt="${
story.title
}" />
<div>
<h3><a target="_blank" href="${story.url}">${story.title}</a></h3>
<p>${story.abstract}</p>
</div>
`;
root.prepend(storyEl);
});
}
Currently were are requesting and displaying the travel section of the New York Times. Our goal is to display different sections depending on the navigation item clicked.
Nav bar clicks wil load new content from the New York Times API, store the data in local storage and render it to the page.
Add categories and navItems variables to index.js
:
import navItemsObject from "./modules/navitems.js";
const categories = navItemsObject.map((item) => item.link);
console.log('categories: ', categories);
const navItems = document.querySelectorAll("li[class^='navitem-']");
console.log('navItems: ', navItems);
Add event listeners to each of the nav items:
for (let i = 0; i < navItems.length; i++) {
navItems[i].addEventListener("click", () => {
fetchArticles(categories[i]);
});
}
IMPORTANT: code order is critical here. The event listeners must be added after the nav items are created (via makeNav()
).
import { makeNav } from "./modules/nav.js";
import navItemsObject from "./modules/navitems.js";
makeNav();
const categories = navItemsObject.map((item) => item.link);
console.log("categories: ", categories);
const navItems = document.querySelectorAll("nav li a");
console.log("navItems: ", navItems);
for (let i = 0; i < navItems.length; i++) {
navItems[i].addEventListener("click", () => {
fetchArticles(categories[i]);
});
}
We will refactor the fetch function to accept a section and use that section in the NYTimes URL:
https://api.nytimes.com/svc/topstories/v2/${section}.json?api-key=${nytapi}
const root = document.querySelector(".site-wrap");
const nytapi = "KgGi6DjX1FRV8AlFewvDqQ8IYFGzAcHM"; // note this should be your API key
// const nytUrl = `https://api.nytimes.com/svc/topstories/v2/${section}.json?api-key=${nytapi}`;
We also store the data in local storage and only request it if it is not already there.
function fetchArticles(section) {
console.log("before", section);
section = section.substring(1);
console.log("after", section);
if (!localStorage.getItem(section)) {
console.log("section not in local storage, fetching");
fetch(
"https://api.nytimes.com/svc/topstories/v2/section.json?api-key=nytapi"
)
.then((response) => response.json())
.then((myJson) => localStorage.setItem(section, JSON.stringify(myJson)))
// .then(renderStories(section))
.catch((error) => {
console.warn(error);
});
} else {
console.log("section in local storage");
// renderStories(section);
}
}
Refactor our renderStories
function to generate html based on the section we passed to the fetch call:
function renderStories(section) {
let data = JSON.parse(localStorage.getItem(section));
if (data) {
data.results.map((story) => {
var storyEl = document.createElement("div");
storyEl.className = "entry";
storyEl.innerHTML = `
<img src="${story.multimedia ? story.multimedia[0].url : ""}" alt="${
story.title
}" />
<div>
<h3><a target="_blank" href="${story.url}">${story.title}</a></h3>
<p>${story.abstract}</p>
</div>
`;
root.prepend(storyEl);
});
} else {
console.log("data not ready yet");
}
}
There is an issue. We click once on the navigation item and the data is loaded but we receive the response "data not ready yet" from the renderStories function.
function fetchArticles(section) {
section = section.substring(1);
if (!localStorage.getItem(section)) {
console.log("section not in local storage, fetching");
fetch(
`https://api.nytimes.com/svc/topstories/v2/${section}.json?api-key=${nytapi}`
)
.then((response) => response.json())
.then((data) => setLocalStorage(section, data))
.catch((error) => {
console.warn(error);
});
} else {
console.log("section in local storage");
renderStories(section);
}
}
function setLocalStorage(section, myJson) {
localStorage.setItem(section, JSON.stringify(myJson));
renderStories(section);
}
Set an active class name:
function setActiveTab(section) {
const activeTab = document.querySelector("a.active");
if (activeTab) {
activeTab.classList.remove("active");
}
const tab = document.querySelector(`li a[href="${section}"]`);
tab.classList.add("active");
}
Call the function
async function renderStories(section) {
setActiveTab(`#${section}`);
// ...
}
Add supporting CSS:
nav a {
opacity: 0.9;
}
nav .active {
font-weight: bold;
opacity: 1;
}
Move everything out of the global scope
(function () {
"use strict";
// Code goes here...
})();