WDD 330: Web Frontend Development II

W02 Team Activity: Dynamic product detail pages

Overview

This activity will review the tools introduced last week, and begin the process of making our application scalable by adding dynamically generated product detail pages, and more organization using ESModules.

Activity Instructions

Complete the following assignment as a team. Designate one team member as the "main driver" and collaborate on their copy of the code. Everyone on the team should be actively engaged in writing the code and contributing to the solution. Once the solution is working, make sure that everyone on the team gets a copy of the code. Each week let someone else be the "main driver" of the coding.

There are many spots where code examples have been given. To get the most out of this activity, do not look at the examples until your group has given that section a try. Then after you look at the example, resist the temptation to copy/paste. Use the examples to get correction, or to help you get unstuck. Do not just use them to get it done.

Core Requirements

Begin the Task - Trello Work

  1. In the synchronous meeting, the driver should visit the team's copy of the project Trello board.
  2. Add each of the attending team members to the Team Activity W02: Dynamic product detail pages card.
  3. Move it to the "Doing" list in Trello.
  4. Read the details of the card together.

GitHub Branch - Driver

  1. Pull any changes from the team GitHub project before proceeding.
  2. Create a new branch named driverinitials--team2 where driverinitials is the driver's initials.

File Structure

We currently have one HTML file for each product we offer. This works okay when there are only 4 products, but the plan is to expand the inventory. We cannot continue with this model. The product pages all follow a similar template. There should be one HTML file that loads the details about the requested product through JavaScript.

Additionally, as our code becomes more complex, we will be well served by organizing the script using ESModules.

IMPORTANT!

Make sure that you are working in the src directory when completing the following items and NOT the dist directory! The build directory will get erased and completely rebuilt each time you run npm run build. Think of dist as the production version of your code. src is your working directory. You should not need to run npm run build very often, like once a week after you have merged some branches.

When you run npm run start it will use the files in src to build the project.

  1. Open one of your current product pages in the editor, and create a new file using File->Save As-> /product-pages/index.html
  2. Add a new file, a module named ProductDetails.mjs in the "js" directory. This script file will contain the code to dynamically produce the product detail pages.
  3. It's very likely that other parts of our application will need to make requests for product data. That functionality has already been exposed in the ProductData.mjs module.
  4. Other general functionality will need to be shared between parts of the application. Notice that there is already an utils.mjs module that will hold that utility functionality.
    Our goal will be to eventually have a one-to-one relationship with HTML views/pages and JS modules. Then any functionality that needs to be shared between modules can be placed in a utility module.

Review ProductData.mjs

Open up the ProductData.mjs module file. Note a few things:

Click for example code (ProductData.mjs)
function convertToJson(res) {
  if (res.ok) {
    return res.json();
  } else {
    throw new Error('Bad Response');
  }
}

export default class ProductData  {
  constructor(category) {
    this.category = category;
    this.path = `../json/${this.category}.json`;
  }
  getData() {
    return fetch(this.path)
      .then(convertToJson).then((data) => data);
  }
  async findProductById(id) {
    const products = await this.getData()
    return products.find((item) => item.Id === id);
  }
}
  1. In order to make our module portable and easy to use, it uses a class to expose all the public facing code from our module. It is called ProductData, and it is the default export.
  2. We will eventually want to use this class for more than just tents. We can do that by using the constructor to the class. If we pass in a category name, e.g., 'tents', the class will store it and also use it to build a path to the correct file
    this.path = `../json/${this.category}.json`;
  3. Notice the getData() method compared to the findProductById() method. One uses just promises and .then(), the other uses async/await. Many find the async/await syntax to be easier to read (and write) than the typical .then() based promise handling.
  4. If the syntax (data) => data is confusing you may want to spend more time reading about arrow functions in JavaScript. You could re-write that short callback function like this using a more traditional anonymous function declaration:
    function(data) { return data; }
  5. The callback (item) => item.Id === id. Another way to write that could be:
    function(item) { return item.Id === id; }
    💡 It is very common to see short functions that just return some value or expression written using the arrow function syntax.
  6. Open up the product.js file and note that the ProductData.mjs module is imported.
    import ProductData from './ProductData.js';
    And then an instance of the ProductData class is then created.
    const dataSource = new ProductData('tents');
    Remember that to use ESModules in JavaScript we have to tell the browser we want to use modules using type="module" in the HTML script tag.

URL Parameters

We need to have some way of passing in the product that we want to show the details for. A common way to do this is through a URL Parameter. A link like this <a href="product_pages/index.html?product=880RR" > has the product id embedded into the link. When the page loads it will have access to that information. If you read through that article it suggests the following lines of code to retrieve it:

const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const product = urlParams.get('product')
  1. Use those lines to create a new function in the utils.mjs file called getParams(param) that we can use to get a parameter from the URL when we need to. (Don't forget to return the parameter!)
  2. Import your new function into product.js.
  3. Open /index.html and change the link to the first product page from href="product_pages/marmot-ajax-3.html" to href="product_pages/?product=880RR".
    Where did the 880RR come from? If you look in the json/tents.json file you will see that each record in there has an id. 880RR just happens to be the id for the first tent.
  4. Then test your getParams function in product.js to see if you can get the product id successfully when someone navigates to the product-details page. This would be a good time to test out our findProductById method as well.
    Click for example code (product.js)
    import ProductData from './ProductData.mjs';
    import { setLocalStorage, getParam } from './utils.mjs';
    const dataSource = new ProductData('tents');
    
    const productId = getParam('product');
    
    console.log(dataSource.findProductById(productId));
  5. You should also update the rest of the links in /index.html to match the first one. You can look up the IDs in the tents.json file.

ProductDetails.mjs

  1. Create a new file in the js folder called ProductDetails.mjs
  2. Model the ProductDetails.mjs file similarly to the ProductData.mjs file by placing the public methods in a class. The following methods are recommended:
    1. constructor(): This is recommended for classes.
    2. init(): There are a few things that need to happen before our class can be used. Some will happen in the constructor and will happen automatically. Others it is nice to have more control over and so we will place them into an init method.
    3. addToCart(): This is the function that is currently in product.js. Move it here.
    4. renderProductDetails(): Method to generate the HTML to display our product.
  3. It will be nice for our product to keep track of important information about itself. For example,
    constructor(productId, dataSource){
      this.productId = productId;
      this.product = {};
      this.dataSource = dataSource;
    }
    With that information the product will know which id it has, it will have a source to get the information it needs when the time comes, and will have a place to store the details we need to show once we retrieve them.
  4. To use this class, pull everything from the last few steps together in the product.js file. Once we get everything refactored that file should look something like this:
    import ProductData from './ProductData.mjs';
    import ProductDetails from './ProductDetails.mjs';
    import { setLocalStorage, getParam } from './utils.mjs';
    
    const productId = getParam('product');
    const dataSource = new ProductData('tents');
    
    const product = new ProductDetails(productId, dataSource);
    product.init();
  5. Notice we import in the code we need from our modules. Then we get the id of our product using our helper function getParams. We create an instance of our ProductData data class with the URL it should use to look for products. Then we use both of those to create an instance of our ProductDetails class so that it has everything it needs to work. Finally we call our init() method using our class instance to finish setting everything up.

Stretch Activity

Finish It Up!

Complete this activity by writing the rest of the code to display our product.

  1. renderProductDetails(): Use the HTML in the /product_pages/index.html as a template to create this function. Once you have the function working remember to remove the HTML from the index.html file so you don't have multiple products showing up!
  2. addToCart(): move the code from product.js for this function and make any changes necessary to make it work.
  3. init(): this function needs to do a few things, copy/paste the following into your code to use as a guide for finishing it:
    async init() {
    // use our datasource to get the details for the current product. findProductById will return a promise! use await or .then() to process it
    // once we have the product details we can render out the HTML
    // once the HTML is rendered we can add a listener to Add to Cart button
    // Notice the .bind(this). Our callback will not work if we don't include that line. Review the readings from this week on 'this' to understand why.
    document.getElementById('addToCart')
      .addEventListener('click', this.addToCart.bind(this));
    }

Instructor's Solution

As a part of this team activity, you are expected to look over a solution from the instructor, to compare your approach to that one.

Do NOT open the solution until you have worked through this activity as a team for at least one hour. At the end of the hour, if you are still struggling with some of the core requirements, you are welcome to view the instructor's solution and use it to help you complete your own code.

After working with your team for the one hour activity, click here for the instructor's solution.

Make a Pull Request.

  1. Complete as much as you can on this activity.
  2. Review the instructor's solution.
  3. Get your code working.
  4. Lint and format your code before committing.
  5. Driver: Commit and push changes to the team project repository.
  6. Driver: Submit a pull request for this driver branch.
  7. Review the pull request as a team.
  8. Close the request and merge the branch back into the Main.
  9. Move the Trello task card to the "Done" list.

Submission

When you have finished this activity, fill out the assessment in I-Learn. You are welcome to complete any additional parts of this activity by yourself or with others after your meeting before submitting the assessment.